How I improved Kartbusiness.com page loading speed from 52 to 94

ยท 1278 words ยท 6 minute read

I used PageSpeed Insights , to detect the performance bottlenicks so I can analyze and fix the performance issues.

Here are all optimizations I did on KartBusiness.com to improve the PageSpeed Insights performance metrics.

run scripts inside web workers ๐Ÿ”—

I offloaded some scripts from running on browser/JS main thread into a web worker by using the partytown library. I specifically offloaded Google Analytics scripts (gtag) and Meta Pixel script (Facebook’s).

I added the library files like this.

<script async>
    partytown = {
        resolveUrl: function(url) {
            const proxyMap = {
                'https://connect.facebook.net/en_US/fbevents.js': 'https://kartbusiness.com/js/fbevents.js',
                'https://connect.facebook.net/signals/config/268514379082591?v=2.9.104&r=stable': 'https://kartbusiness.com/js/fbsignals.js',
            };

            if (proxyMap[url]) {
                return proxyMap[url];
            }
            return url;
        },
        forward: ['dataLayer.push', 'fbq']
    };
</script>
<script async src="{{ asset('/~partytown/partytown.js') }}"></script>

And marked the Google Analytics scripts to be text/partytown not text/javascript like that.

<!-- Google tag (gtag.js) -->
<script type="text/partytown" async src="https://www.googletagmanager.com/gtag/js?id=G-TESTTEST"></script>
<script type="text/partytown" async>
    window.dataLayer = window.dataLayer || [];

    function gtag() {
        dataLayer.push(arguments);
    }
    gtag('js', new Date());
    gtag('config', 'G-TESTTEST');
</script>

And the Meta Pixel Facebook script like that.

<!-- Meta Pixel Code -->
<script type="text/partytown" async>
    ! function(f, b, e, v, n, t, s) {
        if (f.fbq) return;
        n = f.fbq = function() {
            n.callMethod ?
                n.callMethod.apply(n, arguments) : n.queue.push(arguments)
        };
        if (!f._fbq) f._fbq = n;
        n.push = n;
        n.loaded = !0;
        n.version = '2.0';
        n.queue = [];
        t = b.createElement(e);
        t.async = !0;
        t.src = v;
        s = b.getElementsByTagName(e)[0];
        s.parentNode.insertBefore(t, s)
    }(window, document, 'script',
        '//connect.facebook.net/en_US/fbevents.js'); //kartbusiness.com/js/fbevents.js
    fbq('init', '000000000000000');
    fbq('track', 'PageView');
</script>
<noscript>
    <img height="1" width="1" style="display:none"
        src="https://www.facebook.com/tr?id=000000000000000&ev=PageView&noscript=1" />
</noscript>
<!-- End Meta Pixel Code -->

For more detailed information, check out the official partytown documentation.

inline styles ๐Ÿ”—

I copied the font styles and pasted it into an inlined CSS style. Here is the change I made.

<!-- Fonts -->
-<link rel="preconnect" href="https://fonts.googleapis.com">
-<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
-<link href="https://fonts.googleapis.com/css2?family=Tajawal:wght@400;700&display=swap" rel="stylesheet">
+<style>
+    /* arabic */
+    @font-face {
+        font-family: 'Tajawal';
+        font-style: normal;
+        font-weight: 400;
+        font-display: swap;
+        src: url(https://fonts.gstatic.com/s/tajawal/v9/Iura6YBj_oCad4k1nzSBC5xLhLFw4Q.woff2) format('woff2');
+        unicode-range: U+0600-06FF, U+0750-077F, U+0870-088E, U+0890-0891, U+0898-08E1, U+08E3-08FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE70-FE74, U+FE76-FEFC, U+102E0-102FB, U+10E60-10E7E, U+10EFD-10EFF, U+1EE00-1EE03, U+1EE05-1EE1F, U+1EE21-1EE22, U+1EE24, U+1EE27, U+1EE29-1EE32, U+1EE34-1EE37, U+1EE39, U+1EE3B, U+1EE42, U+1EE47, U+1EE49, U+1EE4B, U+1EE4D-1EE4F, U+1EE51-1EE52, U+1EE54, U+1EE57, U+1EE59, U+1EE5B, U+1EE5D, U+1EE5F, U+1EE61-1EE62, U+1EE64, U+1EE67-1EE6A, U+1EE6C-1EE72, U+1EE74-1EE77, U+1EE79-1EE7C, U+1EE7E, U+1EE80-1EE89, U+1EE8B-1EE9B, U+1EEA1-1EEA3, U+1EEA5-1EEA9, U+1EEAB-1EEBB, U+1EEF0-1EEF1;
+    }
+    /* latin */
+    @font-face {
+        font-family: 'Tajawal';
+        font-style: normal;
+        font-weight: 400;
+        font-display: swap;
+        src: url(https://fonts.gstatic.com/s/tajawal/v9/Iura6YBj_oCad4k1nzGBC5xLhLE.woff2) format('woff2');
+        unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+    }
+    /* arabic */
+    @font-face {
+        font-family: 'Tajawal';
+        font-style: normal;
+        font-weight: 700;
+        font-display: swap;
+        src: url(https://fonts.gstatic.com/s/tajawal/v9/Iurf6YBj_oCad4k1l4qkHrRpiZtK6GwN9w.woff2) format('woff2');
+        unicode-range: U+0600-06FF, U+0750-077F, U+0870-088E, U+0890-0891, U+0898-08E1, U+08E3-08FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE70-FE74, U+FE76-FEFC, U+102E0-102FB, U+10E60-10E7E, U+10EFD-10EFF, U+1EE00-1EE03, U+1EE05-1EE1F, U+1EE21-1EE22, U+1EE24, U+1EE27, U+1EE29-1EE32, U+1EE34-1EE37, U+1EE39, U+1EE3B, U+1EE42, U+1EE47, U+1EE49, U+1EE4B, U+1EE4D-1EE4F, U+1EE51-1EE52, U+1EE54, U+1EE57, U+1EE59, U+1EE5B, U+1EE5D, U+1EE5F, U+1EE61-1EE62, U+1EE64, U+1EE67-1EE6A, U+1EE6C-1EE72, U+1EE74-1EE77, U+1EE79-1EE7C, U+1EE7E, U+1EE80-1EE89, U+1EE8B-1EE9B, U+1EEA1-1EEA3, U+1EEA5-1EEA9, U+1EEAB-1EEBB, U+1EEF0-1EEF1;
+    }
+    /* latin */
+    @font-face {
+        font-family: 'Tajawal';
+        font-style: normal;
+        font-weight: 700;
+        font-display: swap;
+        src: url(https://fonts.gstatic.com/s/tajawal/v9/Iurf6YBj_oCad4k1l4qkHrFpiZtK6Gw.woff2) format('woff2');
+        unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+    }
+</style>

HTML entities ๐Ÿ”—

I used HTML entities instead of SVGs and PNGs to send less characters through the wire. The change I made is like that.

-<svg aria-hidden="true" data-prefix="fas" data-icon="angle-left"
-    class="inline-block text-gray-600 dark:text-gray-300" xmlns="http://www.w3.org/2000/svg"
-    height="18" width="18" viewBox="0 0 256 512"
-    {{ app()->isLocale('en') ? 'transform=rotate(180)' : '' }}>
-    <path fill="currentColor"
-        d="M31.7 239l136-136c9.4-9.4 24.6-9.4 33.9 0l22.6 22.6c9.4 9.4 9.4 24.6 0 33.9L127.9 256l96.4 96.4c9.4 9.4 9.4 24.6 0 33.9L201.7 409c-9.4 9.4-24.6 9.4-33.9 0l-136-136c-9.5-9.4-9.5-24.6-.1-34z" />
-</svg>
+&rsaquo;

Read more about that case here .

preconnects domains ๐Ÿ”—

Preconnecting helps to get the files faster. So, I preconnect fonts/styles URLs like this.

<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>

I also use unpkg CDN, so I preconnect to its domain like that.

<link rel="preconnect" href="https://unpkg.com" crossorigin>

lazyload images ๐Ÿ”—

Lazyloading the images means that the image will be loaded right before the user scrolls to see it. All modern web browsers support native lazyloading of images. To take advantage of that, I add loading='lazy' in the image tag like that.

-<img src="image/path/here" alt="describe the image in words">
+<img loading="lazy" src="image/path/here" alt="describe the image in words">

And I added that in the <picture> element like this.

<picture>
    <source srcset="/img/default-portfolio.webp" type="image/webp">
    <source srcset="/img/default-portfolio.png" type="image/png">
-    <img width="1000" height="1000"
+    <img loading="lazy" width="1000" height="1000"
        src="/img/default-portfolio.png"
        alt="description of the image">
</picture>

remove obsolete code ๐Ÿ”—

I removed not-working useless feature which was heavy on Javascript.

run scripts asynchronously ๐Ÿ”—

I added async attribute to let the scripts run asynchronously.

<!-- Scripts -->
-<script src="{{ mix('js/app.js') }}"></script>
+<script async src="{{ mix('js/app.js') }}"></script>

run scripts after all things loaded ๐Ÿ”—

I added defer attribute to let some scripts to run after the page finished loading.

- <script src="/js/some-script.js"></script>
+ <script src="/js/some-script.js" defer></script>

set image width and height ๐Ÿ”—

Setting the image width and height is great to fix layout shifts.

- <img src="/img/eg.svg" alt="Egypt" title="Egypt">
+ <img src="/img/eg.svg" width="20" height="15" alt="Egypt" title="Egypt">

And for picture tags, I do.

<picture>
    <source srcset="/img/default-team-member.webp" type="image/webp">
    <source srcset="/img/default-team-member.png" type="image/png">
-    <img loading="lazy"
+    <img loading="lazy" width="150" height="150"
        src="/img/default-team-member.png" alt="default team member avatar">
</picture>

DRY codebase ๐Ÿ”—

Instead of rewriting the same UI code too many times, create a template or component then reuse it. So your codebase will be more DRY. DRY stands for “Don’t Repeat Yourself”.

remove ads ๐Ÿ”—

I removed ads as it is heavy on webpage loading yet it is not good enough in monetization. I just did it in my case. Maybe your case is different.

UI contrast ratio ๐Ÿ”—

PageSpeed Insights website helped me find too many UI elements/tiles especially in light mode which has not enough contrast ratio. I fixed these user interface issues by making sure that the text is readable on the background in all these cases and elements.

update & upgrade deps & libs ๐Ÿ”—

I upgraded npm dependecies and libraries via npm update. I did not found updates/upgrades for other package manager libs/deps such as PHP Composer.

All optimization in a brief table ๐Ÿ”—

web page link//cards/1/blog/blog/pro-cardaverage
starting point6148544652.25
partytown gtag7360866069.75
partytown Meta Pixel6664806869.5
font into inline style7364.5806470.375
preconnect font’s url7068786470
lazyload more images8071.25696571.3
remove save_card feature7664.75777472.9
async+defer JS scripts6864.75756167.18
img width/height, undefer, async7165.75756769.687
undefer ads JS script7477816273.5
DRY buttons7570906374.5
remove ads8798958390.75
fix UI issues9297948592
upgrade npm deps/libs9698968694

Notes ๐Ÿ”—

  • The number in /cards/1 fields is an average of 50 single card pages tested. I tested 50 webpages and got the average. These pages are /cards/1, /cards/30 and /cards/50 and so on.
  • The number in /blog/pro-card fields is an average of 5 blog post pages.
  • I used average numbers for better accuracy.

So, by make all the above steps, I improved Kart Business website metric from 52.25 to 94 ๐ŸŽ‰

I hope this post helps you. If you know a person who can benefit from this information, send them a link of this post. If you want to get notified about new posts, follow me on YouTube , Twitter (x) , LinkedIn , Facebook , Telegram and GitHub .

Share: