<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Design Community: Metehan Altuntekin</title>
    <description>The latest articles on Design Community by Metehan Altuntekin (@metehandesign).</description>
    <link>https://design.forem.com/metehandesign</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F822943%2Fc35939c2-44ae-4c16-adb0-118ae5364593.jpeg</url>
      <title>Design Community: Metehan Altuntekin</title>
      <link>https://design.forem.com/metehandesign</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://design.forem.com/feed/metehandesign"/>
    <language>en</language>
    <item>
      <title>How I optimized my SvelteKit website from 70 to 95+ on mobile Lighthouse tests</title>
      <dc:creator>Metehan Altuntekin</dc:creator>
      <pubDate>Fri, 06 Mar 2026 16:47:30 +0000</pubDate>
      <link>https://design.forem.com/metehandesign/how-i-optimized-my-sveltekit-website-from-70-to-95-on-mobile-lighthouse-tests-1ba3</link>
      <guid>https://design.forem.com/metehandesign/how-i-optimized-my-sveltekit-website-from-70-to-95-on-mobile-lighthouse-tests-1ba3</guid>
      <description>&lt;p&gt;When I initially built this portfolio/blog website, my priority was getting it live quickly, so I didn't pay much attention to performance. The stack I used — SvelteKit and TailwindCSS — handled a good baseline but tools alone can't cover everything. Lighthouse desktop scores were about 95-98 but mobile scores were only around 65-80.&lt;/p&gt;

&lt;p&gt;The mobile tests are very harsh. They simulate a slow 4G network and a very throttled CPU. So many things that are negligible on desktop become prominent on mobile. I had to put in work to improve these things to make the mobile test scores better. I also wanted to expand my knowledge of website loading performance, so I decided to take on this project.&lt;/p&gt;

&lt;p&gt;And I finally achieved what I wanted. Now after all this work, my test scores are consistently 95+ on mobile and 100 on desktop. And I learned quite a few things along the way. With this post, I want to publish my journey and the things I learned. The topics will be varying from why I chose JPEG instead of more optimized formats for my LCP image, to tricks in SvelteKit to make it load more efficiently for websites with smaller bundle sizes.&lt;/p&gt;

&lt;p&gt;I hope it will be useful for future me who may forget these and everyone else who is reading this.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before State
&lt;/h2&gt;

&lt;p&gt;Let's start with taking a look at one of the mobile test results from the before state.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvbcgfqu06zqhum4lj3z5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvbcgfqu06zqhum4lj3z5.png" alt="before performance scores" width="800" height="532"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The FCP is not too bad with 1.7 seconds. We also don't see any CLS, which is great.&lt;/p&gt;

&lt;p&gt;But our LCP is 5.9 seconds, which is bad. TBT is 220ms and Speed Index is 3.9 seconds, these don't look good either.&lt;/p&gt;

&lt;p&gt;You can find these results &lt;a href="https://pagespeed.web.dev/analysis/https-9c949a03-my-portfolio-v2-8as-pages-dev/fywei63hog?form_factor=mobile" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Analyzing Performance with Lighthouse
&lt;/h2&gt;

&lt;p&gt;A great thing about Lighthouse is that it gives insights and diagnostics that help us understand the problems and culprits. So let's begin with taking a look at them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy7aswn3ie3k72o2pe3ux.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy7aswn3ie3k72o2pe3ux.png" alt="before insights &amp;amp; diagnostics" width="800" height="702"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Lighthouse is giving us quite a few insights and diagnostics. Which is great, we can use this information to figure out what we need to do.&lt;/p&gt;

&lt;p&gt;The screenshots also show us that it is taking quite a while for the site to even start rendering. This means the browser is taking too long to download all the necessary assets and process them to begin with rendering.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reviewing Insights &amp;amp; Diagnostics
&lt;/h3&gt;

&lt;p&gt;Let's review each of the insights one by one and try to understand what they mean.&lt;/p&gt;

&lt;p&gt;Some of these insights turned out to be caused by my PostHog implementation. PostHog is a separate topic, so I will exclude those insights from this post. I eventually decided to remove it but also experimented with it a bit. I plan to write a separate post about that later.&lt;/p&gt;

&lt;h4&gt;
  
  
  Improve image delivery
&lt;/h4&gt;

&lt;p&gt;Images are usually the low hanging fruit when optimizing website performance as they take quite a bit of bandwidth and processing. And often at least one of them is the LCP element of the initial load screen, which makes it even more important.&lt;/p&gt;

&lt;p&gt;In my case, I have one image of myself in my hero section. We can see that Lighthouse is showing us some problems about it:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx1cpotvpqusnrfxgvqh4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx1cpotvpqusnrfxgvqh4.png" alt="Insight: image delivery" width="800" height="663"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We can see that Lighthouse is suggesting a few things. First is that the hero image at 132.4KiB is quite big for this size. It could be compressed more. Also JPEG compression format is not resulting in the smallest file size (though decoding JPEG is the fastest, more on that later).&lt;/p&gt;

&lt;p&gt;Then, we are seeing that a second image is being loaded too. And Lighthouse tells us this image could be compressed further and resized smaller. Probably true, but the real problem here is this image being loaded in the first place. Why? It's because this image belongs to the "Projects" section, which is not visible in initial load. But it comes right after the hero section, which seems to make the browser prioritize it.&lt;/p&gt;

&lt;p&gt;This means we will need to optimize the hero section image and find a solution to deprioritize the rest of the images.&lt;/p&gt;

&lt;h4&gt;
  
  
  Network Dependency Tree
&lt;/h4&gt;

&lt;p&gt;Network dependency trees happen when a resource (a file) triggers to load another resource. This results in extra delays with waiting for all the loading and processing time of a resource before it can load the next one.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg4pcm2mn98hzr0kd526b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg4pcm2mn98hzr0kd526b.png" alt="Insight: network dependency tree" width="800" height="507"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We can see that &lt;code&gt;assets/0.C2pj507j.css&lt;/code&gt; takes 266 milliseconds with 9.66KiB and &lt;code&gt;assets/4.DgPqur8T.css&lt;/code&gt; takes 261 milliseconds with 1.14KiB. These are just CSS chunks but their download is triggered only after the initial HTML file is loaded and processed to detect the links to these stylesheets. So, even though Tailwind and Vite optimizes the CSS files, they still add to our network usage.&lt;/p&gt;

&lt;h4&gt;
  
  
  Render Blocking Requests
&lt;/h4&gt;

&lt;p&gt;Render blocking requests happen when a file being loaded is required to be processed by browser to render the website properly. These files are generally CSS files.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faj3xvim596e4cdvppv92.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faj3xvim596e4cdvppv92.png" alt="Insight: render blocking requests" width="800" height="218"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And in our case, the same CSS files in the Network dependency tree issue are also blocking render until they are downloaded. It makes sense, since CSS is a required asset for the browser to render the website. So the solution we will come up with can fix both of these issues at once.&lt;/p&gt;

&lt;h4&gt;
  
  
  LCP Breakdown
&lt;/h4&gt;

&lt;p&gt;"LCP breakdown" isn't a warning but it is a useful insight because it shows us what element is considered as the LCP element and it also shows us the details of its loading.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frmm7lz59pgfeiovhg1a4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frmm7lz59pgfeiovhg1a4.png" alt="Insight: LCP breakdown" width="800" height="289"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Turns out the detected LCP element is not the hero section image as I was expecting. Instead, the subheading text element is considered as the LCP element by the browser. This means the image is smaller than this text element. And I guess image is still taking a little longer than this text to render. That means our current LCP score may even be worse than calculated 5.9 seconds.&lt;/p&gt;

&lt;p&gt;We see that the &lt;code&gt;Element render delay&lt;/code&gt; is 2,290 milliseconds. That is 2.29 seconds of browser just doing nothing about the LCP element while it's busy on other things. That is a huge waste of time. But it is not a surprise now that we know what things are blocking the render.&lt;/p&gt;

&lt;p&gt;This also will get better with all the solutions we will come up for the previous issues.&lt;/p&gt;

&lt;h4&gt;
  
  
  3rd Parties
&lt;/h4&gt;

&lt;p&gt;3rd party code is scripts injected with a different source than your website's domain.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhv4kpd6r6v9048x5zc44.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhv4kpd6r6v9048x5zc44.png" alt="Insight: 3rd Parties" width="800" height="359"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I am using &lt;code&gt;@iconify/svelte&lt;/code&gt; so it is adding lines where it loads the icons from Iconify CDN. The total transfer size is 18KiB while main thread time is 0ms. They likely load after everything else so I am not too worried about this at the moment.&lt;/p&gt;

&lt;h4&gt;
  
  
  Avoid long main-thread tasks
&lt;/h4&gt;

&lt;p&gt;Long main-thread tasks happen when there is a task that requires a lot of processing in the main-thread.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyoe4eebv0eqbeu7qllwl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyoe4eebv0eqbeu7qllwl.png" alt="Diagnostic: Avoid long main-thread tasks" width="800" height="243"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;PostHog session recorder is being the biggest culprit here, which will be solved once I remove it. But after that, a chunk of our build seems to be taking some time to start. That's probably because all the network usage causing this one to take long to load. But it also takes 95ms to run which is a significant duration worth paying attention to.&lt;/p&gt;

&lt;p&gt;I then took a look at the &lt;code&gt;chunks/ClkWq1G8.js&lt;/code&gt; to see what it is about. It seems to be mostly related to Iconify and injecting the SVGs it downloads into our DOM. So we may want to do something about that, perhaps moving away from &lt;code&gt;@iconify/svelte&lt;/code&gt; package. The icons loading and being added later can also cause layout shifts which we don't want and it also makes us rely on Iconify's CDN.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding optimization solutions
&lt;/h2&gt;

&lt;p&gt;Now that we reviewed the issues and learned from the Lighthouse insights &amp;amp; diagnostics, we can start looking into the solutions.&lt;/p&gt;

&lt;p&gt;The most important thing I need to focus on is to reduce the downloading and processing time of our assets. We saw in the insights that a piece of text as an LCP element waits for 2.3 seconds to even start rendering, which is very bad. But it will be reduced as I improve the network and processor usage.&lt;/p&gt;

&lt;p&gt;It is also important to note that we want the scores to be stable between different tests. Networks have slight instabilities which can sometimes amplify some issues. So getting consistent scores is also as important as raising them up.&lt;/p&gt;

&lt;p&gt;Let's delve into each topic one by one.&lt;/p&gt;

&lt;h3&gt;
  
  
  CSS Inlining in SvelteKit
&lt;/h3&gt;

&lt;p&gt;There are a few things we can improve on the CSS loading. One is that, they are being triggered to load after the HTML is loaded and this makes Lighthouse give us the &lt;code&gt;Network dependency tree&lt;/code&gt; warning. The second is that they are blocking the render until they finish loading and this triggers Lighthouse to give &lt;code&gt;Render blocking requests&lt;/code&gt; warning.&lt;/p&gt;

&lt;p&gt;To make this better, we can configure our setup to keep the CSS files inlined in the HTML file. This will save us from the dependency tree slowness as all the HTML and required CSS will be downloaded in one single batch. It will make our HTML files bigger, but it will certainly perform better at initial load than the same size being loaded separately and dependently.&lt;/p&gt;

&lt;p&gt;In order to do this, we can just tell Svelte compiler to inline our CSS. We can do that by adding this configuration option in our Svelte configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// svelte.config.js&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;//...&lt;/span&gt;
    &lt;span class="na"&gt;kit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// ...&lt;/span&gt;
        &lt;span class="na"&gt;inlineStyleThreshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10240&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Inline CSS files smaller than 10KiB&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;This tells Svelte to inline CSS files smaller than 10KiB. Our biggest CSS file is under this threshold with 9.4kB so it should inline it. Right?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feklafck4w7iy4fvmsxqg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feklafck4w7iy4fvmsxqg.png" alt="CSS inlining 10kB threshold result" width="800" height="94"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wrong. It didn't work.&lt;/strong&gt; It included the smaller 1.7kB one but it did not include the 9.4kB one. What's going on here?&lt;/p&gt;

&lt;p&gt;Well, it turns out the 9.4kB one wasn't actually 9.4kB on disk. That size was only the data transferred through the network, the compressed version. But the raw disk size was 52.43kB. Apparently Svelte looks at the disk size, not the compressed size transferred, naturally.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcky83424m99qzsmdpod8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcky83424m99qzsmdpod8.png" alt="disk size of the compressed 9.4kB file" width="202" height="68"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So I think I should set the threshold to 60kB instead to make it include this file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// svelte.config.js&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;//...&lt;/span&gt;
    &lt;span class="na"&gt;kit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// ...&lt;/span&gt;
        &lt;span class="na"&gt;inlineStyleThreshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Inline CSS files smaller than 60kB&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And yes it worked, now finally all of the CSS files are inlined:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv5apv2h47wqpe22vqrvx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv5apv2h47wqpe22vqrvx.png" alt="Results with 60kB threshold" width="800" height="80"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This increased the size of &lt;code&gt;index.html&lt;/code&gt; with jumping from ~6kB to 15.4kB but it is worth it for this case. And since this loads in the first single request, we have successfully eliminated network dependency tree and render blocking request issues. This also reduced the network stability issues thanks to not having to chain CSS files after the HTML is processed.&lt;/p&gt;

&lt;p&gt;One thing to pay attention to here is that, this breaks the caching of the CSS completely. So, when a visitor navigates between pages, all the content of the CSS load from scratch again with each .html file. For a content website like this it is not important, but it can be a problem for SaaS apps with lots of users.&lt;/p&gt;

&lt;h3&gt;
  
  
  Image optimization
&lt;/h3&gt;

&lt;p&gt;As we saw in the Lighthouse insights, the LCP element wasn't the hero section image as I guessed, it was instead the subheading text underneath the heading. This made me realize I missed the hierarchy of design there a bit. So I decided to make the image slightly bigger, now making it the LCP element.&lt;/p&gt;

&lt;p&gt;The 132.4kB size of the image wasn't properly optimized. That wasn't horrible but it wasn't in the good range either. I took a bit of work through optimizing it and finally got it down to 37kB.&lt;/p&gt;

&lt;p&gt;I firstly improved the responsive image set I already had. Responsive images with multiple different sources allow the browser to select the image size it calculates for the area it will print the image. You don't need the 1400px size — that you may need on a 4K monitor — in a little handheld device.&lt;/p&gt;

&lt;p&gt;I also — even though they result in smaller file sizes — eventually decided against using AVIF or WebP on mobile, while choosing progressive JPEG instead. WebP was destroying the colors of my image no matter what I tried and AVIF was lowering the scores because it was much slower to decode. So AVIF was ending up the slower option on a very throttled CPU, despite the smaller size.&lt;/p&gt;

&lt;p&gt;I plan to write a more detailed separate post about this later.&lt;/p&gt;

&lt;h3&gt;
  
  
  Font optimization
&lt;/h3&gt;

&lt;p&gt;Font files are very important to be aware of as they can be quite large. I prefer to self host them (rather than using a CDN like Google Fonts) for best control and reliability so I had to work on optimization myself.&lt;/p&gt;

&lt;p&gt;First things first, when you define a font face you should probably set your  &lt;code&gt;@font-face&lt;/code&gt; to &lt;code&gt;font-display: swap;&lt;/code&gt;. What this does is, it swaps the font family you specified with the next one you define until the font file loads. This helps prevent blocking of rendering until your font file loads.&lt;/p&gt;

&lt;p&gt;I use Commissioner variable font for one single font file for my website. It’s a very stylish font that doesn’t feel cold and it's also readable in long reading text. It can be configured to very different styles thanks to its generous offering of variable axes. You can read the review of it by &lt;a href="https://pimpmytype.com/font/commissioner" rel="noopener noreferrer"&gt;PimpMyType here&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But having all these axes also comes with a downside. They make the variable font file quite big. The TTF version of variable file is massive at 740kB size.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvzf87rna7ittw1hugdql.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvzf87rna7ittw1hugdql.png" alt="Disk size of the variable TTF file" width="800" height="40"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But we don't really need to use TTF anymore, do we? No, we have WOFF2 format now. After compression to WOFF2, the same file is down to 269kB. Much smaller than original but it's still very big though.&lt;/p&gt;

&lt;p&gt;That's what I have been using until this performance optimization project. Even though I had the &lt;code&gt;font-display: swap;&lt;/code&gt;, this font file with 269kB size was using quite a bit of network. And it was in the priority so it was slowing down the loading of the other assets. So I decided to shrink it down further with font subsetting.&lt;/p&gt;

&lt;p&gt;The tool I have used was &lt;code&gt;pyftsubset&lt;/code&gt;. I don't want to get into the details of font subsetting here but I may write another post in the future on how I did it.&lt;/p&gt;

&lt;p&gt;Eventually, with font subsetting, I got this font file down to 99kB. And that is while all the variable axes still included. I could probably go much lower than that if I'd give up on the axes but I didn't want to do that. And this shaved off 170kB of dead weight.&lt;/p&gt;

&lt;h3&gt;
  
  
  Replacing 100 divs with image tiling (spoiler: didn't help)
&lt;/h3&gt;

&lt;p&gt;I also found a potential performance issue in my code that Lighthouse couldn't have shown me.&lt;/p&gt;

&lt;p&gt;I have a background grid decoration in the home page in a particular style. It is a pattern that consists of a solid and a dashed line alternating on both horizontal and vertical directions to create an interesting blueprint style background decoration. The decoration also fades away towards the edges with a CSS mask.&lt;/p&gt;

&lt;p&gt;The method I used to implement this in code was, creating divs for each line (about 50 in each axis) and styling them with CSS, aligning them with flexbox, using a linear gradient for the dashed lines and solid background color for solid ones. On top of that also a shadow of the same color as the line color on each line to make it softer.&lt;/p&gt;

&lt;p&gt;This method was pretty expensive as it was both requiring heavy CSS processing and bloating the DOM with 100 extra divs. So I had to change that to a more efficient method.&lt;/p&gt;

&lt;p&gt;As a solution, I implemented the same grid with background image tiling instead. I created an image for a square that forms as a part of this pattern in Figma. Then I exported it and converted to WebP. That left me with a small 730B image. Then I used it as a background image with the combination of &lt;code&gt;background-repeat: repeat;&lt;/code&gt; to make it tile. That resulted in the same thing I made with the div lines, but with a lot more efficiency — reducing both &lt;code&gt;index.html&lt;/code&gt; file size and the time browser needs to render the decoration. &lt;a href="https://github.com/Metehan-Altuntekin/my-portfolio/blob/master/src/lib/components/GridBG.svelte" rel="noopener noreferrer"&gt;You can find the code for that component here.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And after testing, I couldn't see any measurable difference with this update. I guess modern browser engines handle CSS and DOM rendering so efficiently even on throttled processors, so 100 divs with shadows and gradients weren't a bottleneck I assumed it would be. It also didn't reduce the HTML size much at all, only 0.3kB.&lt;/p&gt;

&lt;p&gt;I am keeping this regardless, since it resulted in cleaner code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Migrating to unplugin-icons from @iconify/svelte
&lt;/h3&gt;

&lt;p&gt;I was using &lt;code&gt;@iconify/svelte&lt;/code&gt; to render my icons. It is a pretty convenient npm package that helps me add any icon or change it with just changing the icon code.&lt;/p&gt;

&lt;p&gt;But it had some performance downsides. It was causing 18KiB of requests to Iconify CDN, the JS chunk that was performing the addition of the icons to the DOM was blocking the main thread for 95ms. And it also had a potential for layout shifts because it was adding the icons after rendering started.&lt;/p&gt;

&lt;p&gt;So I found &lt;a href="https://github.com/unplugin/unplugin-icons" rel="noopener noreferrer"&gt;unplugin-icons&lt;/a&gt; as an alternative. It does things differently than &lt;code&gt;@iconify/svelte&lt;/code&gt;. Instead of loading from a CDN and hydrating the DOM on the client side, it adds the SVGs directly into the .html file in the build. And we still get to keep the benefits of importing icons from Iconify with their ids, but only from &lt;code&gt;@iconify/json&lt;/code&gt; locally this time.&lt;/p&gt;

&lt;p&gt;This also has a few trade-offs. First is that it removes separate caching for icons as they are bundled in the .html file. And then, for my case, &lt;code&gt;index.html&lt;/code&gt; file size increased from the previous 15.5kB to 26.8kB. These trade-offs weren't important compared to the benefits it provided for my use case — mainly the 95ms of main thread blocking — so I was fine with this.&lt;/p&gt;

&lt;p&gt;A better method could be creating SVG sprites manually and importing all the icons with just one single file. This can especially be used for lazy loading them if the icons are not shown at the first loading screen, while also keeping the caching benefits. However, I like the convenience of still being able to use Iconify icons directly inside my components so I didn't bother to migrate to sprites, at least for this website.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reducing the quantity of the chunks
&lt;/h3&gt;

&lt;p&gt;SvelteKit uses granular chunking of Rollup by default to use caching more efficiently for JS chunks. With granular chunking, it generates smaller but many chunks to isolate specific things like a &lt;code&gt;node_modules&lt;/code&gt; entry. It also uses deterministic hashing to persist the same name for a chunk with the same exact content between builds.&lt;/p&gt;

&lt;p&gt;This is great for caching but it has a slight downside. It increases the quantity of chunks, which makes the compression and client processing of each file slightly less efficient. It is not as bad as it would be in the old days with HTTP/1.1 thanks to new HTTP/2 and HTTP/3 multiplexing allowing many files to be sent over one connection. But each file still has header overhead and requires the browser to manage a separate entry in the network stack.&lt;/p&gt;

&lt;p&gt;And on a narrow bandwidth or a very slow mobile CPU, the overhead of parsing more headers and managing more streams can still cause extra delays.&lt;/p&gt;

&lt;p&gt;In this case the chunk total size was 64.6KiB while the quantity of chunks was 28.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxv28fdgr8e07u87wyg9u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxv28fdgr8e07u87wyg9u.png" alt="Network waterfall on HTTP/3 with 28 chunks" width="800" height="812"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So I wanted to reduce the quantity of these chunks. Best opportunity for that was creating a &lt;code&gt;vendor.js&lt;/code&gt; to bundle the &lt;code&gt;node_modules&lt;/code&gt; entries to one single file. So I added this bit of config to my setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// vite.config.ts&lt;/span&gt;

&lt;span class="c1"&gt;// ones to not include in the vendor&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;heavyLibs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt; &lt;span class="c1"&gt;// e.g. "three"&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;rollupOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="c1"&gt;// bundle small node_modules packages into one single .js file&lt;/span&gt;
                &lt;span class="c1"&gt;// to reduce the amount of the chunks&lt;/span&gt;
                &lt;span class="na"&gt;manualChunks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

                    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node_modules&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

                        &lt;span class="c1"&gt;// still bundle heavy ones separately&lt;/span&gt;
                        &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lib&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;heavyLibs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                            &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lib&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;lib&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                        &lt;span class="p"&gt;}&lt;/span&gt;

                        &lt;span class="c1"&gt;// everything else to vendor&lt;/span&gt;
                        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vendor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                    &lt;span class="p"&gt;}&lt;/span&gt;

                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes Rollup bundle all the &lt;code&gt;node_modules&lt;/code&gt; entries in one single file (except for the ones defined in &lt;code&gt;heavyLibs&lt;/code&gt;). And this results in fewer chunks. The name of the script in production will be hashed so don't be surprised if you can't find &lt;code&gt;vendor.js&lt;/code&gt; in the build folder.&lt;/p&gt;

&lt;p&gt;This method can be counterintuitive if you include big heavy libraries like &lt;code&gt;three&lt;/code&gt; in this as it may inflate this chunk. So I also added a constant to define them to not include in this bundle. I don't have any big packages so mine is empty.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjd85b8iq04r07jdrtz0z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjd85b8iq04r07jdrtz0z.png" alt="Network waterfall on HTTP/3 with 12 chunks" width="800" height="814"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It will also change the &lt;code&gt;vendor.js&lt;/code&gt; chunk each time we update a dependency. So this means we miss the granular caching for the &lt;code&gt;node_modules&lt;/code&gt;, which is a trade-off that I don't find worth worrying for this specific case.&lt;/p&gt;

&lt;p&gt;This method got the quantity of the individual chunks down to 12 from 28. It also allowed compression algorithms to reduce the total size of JS chunks from 64.6KiB to 59.1KiB, though this little of a difference won't really matter. It makes the loading more stable, because fewer files means less getting affected by network jitter and less processing for the slow processor.&lt;/p&gt;

&lt;h3&gt;
  
  
  Removing preload of JS files in SvelteKit
&lt;/h3&gt;

&lt;p&gt;After all this, I was seeing some drops in the scores in some tests. These low score test results had a suspicious "Element render delay" of sometimes over 2 seconds, while the good ones had only about 120ms.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkihwv6crsun9s0t3clx1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkihwv6crsun9s0t3clx1.png" alt="LCP breakdown insight" width="800" height="289"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I realized that the JS chunks were getting more priority, despite the preload tag of the image. That was due to SvelteKit setting the chunks to preload too. So the chunks were being treated with higher priority than the image by the browser.&lt;/p&gt;

&lt;p&gt;Network is an unstable thing. So sometimes due to network differences I was getting 2 seconds of element render delay on my LCP image instead of 120ms that happens on a good time. The quantity of the files causing more competition and CPU usage was affecting the load time.&lt;/p&gt;

&lt;p&gt;So it was important to make the LCP image higher priority. I tried to disable CSR first and that made a massive difference as it removed all the chunks. But I didn't really want to remove CSR, that would break the beautiful seamless navigation of SvelteKit within my website.&lt;/p&gt;

&lt;p&gt;Then I discovered this method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// hooks.server.ts&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleRemovePreloads&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Handle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;resolve&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;preload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="c1"&gt;// 1. Preload start.js and app.js&lt;/span&gt;
                &lt;span class="c1"&gt;// These are pretty small and essential for booting the app.&lt;/span&gt;
                &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;entry/start&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;entry/app&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;

                &lt;span class="c1"&gt;// 2. Other .js files don't preload&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="c1"&gt;// Keep other assets&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a little server handle hook that tells Svelte what to allow to preload and what not to. I set to allow the preload of &lt;code&gt;app.js&lt;/code&gt; and &lt;code&gt;start.js&lt;/code&gt; since these two are the essential main scripts. But every other JS file can wait after the LCP image. The other assets like CSS files are allowed.&lt;/p&gt;

&lt;p&gt;What this essentially does is that, it hides the rest of the scripts from the initial preload scanner of the website. Only the &lt;code&gt;start.js&lt;/code&gt; and &lt;code&gt;app.js&lt;/code&gt; link tags get added in the head. SvelteKit injects the link tags for the rest of the chunks later. And this lets the preload tags we have for the hero section image to be processed before these scripts, causing the browser to fetch it first.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fut57n1aaufjdi50q1otp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fut57n1aaufjdi50q1otp.png" alt="Network waterfall after JS chunks removed from preload (localhost HTTP/1.1)" width="800" height="820"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After this method, my LCP image started to load immediately after the .html file was done loading. After we get our LCP complete, the rest of the .js files start loading. We still load all of our 12 JS files but in a different order.&lt;/p&gt;

&lt;p&gt;This method alone as the last step brought my live site test results from 92-96 to stable 95-98. I was so glad to find it, there was no functionality loss and the PSI was showing the high range we wanted constantly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;Now let's take a look at the after test results and see how much better this gets.&lt;/p&gt;

&lt;p&gt;This is one of the results from PageSpeed Insights:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh05lcsw7uzcf6aavm1vw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh05lcsw7uzcf6aavm1vw.png" alt="After scores on PSI" width="800" height="694"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;98 out of 100. Total blocking time is completely eliminated. FCP went down to 1 second. Speed index went down to 1.2 seconds. 2.4 seconds of LCP. Pretty great scores overall. You can find this test's results &lt;a href="https://pagespeed.web.dev/analysis/https-metehan-design/1poy0njydp?form_factor=mobile" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The results now sit between 95-98 consistently, which is the consistency I wanted. It is possible that I can take this even further but I would probably have to make compromises like removing some variable axes from my font file or disabling CSR — trade-offs that aren't worth it.&lt;/p&gt;

&lt;p&gt;And with local Lighthouse testing (with &lt;code&gt;npx @lhci/cli&lt;/code&gt; median of 10 tests, with &lt;code&gt;--settings.throttlingMethod=devtools&lt;/code&gt;):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fubwya14y4489dea59e80.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fubwya14y4489dea59e80.png" alt="Localhost preview median of 10 tests" width="800" height="586"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;99 out of 100 on mobile. 0.9s FCP, 2.1s LCP, 0ms TBT, 0 CLS, 1.1s SI. Excellent.&lt;/p&gt;

&lt;p&gt;These results are even better due to the differences in the testing environments. Local testing with a localhost preview gives the most reliable results as it has zero network connection issues, but its downside is that it uses HTTP/1.1 compared to Cloudflare hosted HTTP/2 and HTTP/3.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;With all this work, I finally achieved the consistent 95+ mobile scores and learned a lot of things. Lighthouse was very useful to spot issues, but it doesn't show everything. Some of the improvements required me to be aware of the details in the code.&lt;/p&gt;

&lt;p&gt;Optimizing basics like font files and images was crucial. After that, setup-specific solutions like CSS inlining and especially the JS chunks preloading trick have been very helpful. In the end, initial loading performance boils down to reducing the size and the processing time of assets.&lt;/p&gt;

&lt;p&gt;It is important to mention these setup-specific solutions each come with trade-offs and they may not be ideal for all cases. They have been pretty useful for a small content website like this, but a complex SaaS app with hundreds of daily users will have different needs.&lt;/p&gt;

&lt;p&gt;I also focused mostly on the lab test results on this project. Lab tests don't cover everything but they are very important tools to predict real-world performance. Since Lighthouse mobile tests were very throttled, now I know that my website will load fast even in the worst conditions.&lt;/p&gt;

</description>
      <category>sveltekit</category>
      <category>webperf</category>
      <category>lighthouse</category>
      <category>frontend</category>
    </item>
    <item>
      <title>What my dining table and my cat showed me about human attention</title>
      <dc:creator>Metehan Altuntekin</dc:creator>
      <pubDate>Mon, 05 Jan 2026 15:42:51 +0000</pubDate>
      <link>https://design.forem.com/metehandesign/what-my-dining-table-and-my-cat-showed-me-about-human-attention-3c99</link>
      <guid>https://design.forem.com/metehandesign/what-my-dining-table-and-my-cat-showed-me-about-human-attention-3c99</guid>
      <description>&lt;p&gt;We all think that we see a thing as it is when we look at it. But this is not true. We see things how we look at them.&lt;/p&gt;

&lt;p&gt;I just realized this while looking at the patterns on the dining table in my kitchen:&lt;/p&gt;

&lt;p&gt;When I look at this image for ascending line patterns, I see columns. When I look for descending line patterns, I see those instead. When I look for the V-shape formed by both, I see those lines; when I look for the A-shape, I see them as columns. When I look for both separately, I see both but with less focus—likely because my brain shifts from one to another rapidly.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm0a0jezcx8n192nm3uk4.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm0a0jezcx8n192nm3uk4.jpeg" alt="Dining table pattern" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Our brains only see what they look for.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;My cat, Hureyre, doesn't see this at all. He only sees the piece of meat or butter I put on the table. He doesn't see text on paper or patterns on bed sheets. He doesn't notice the different clothes I wear. He has only a few interests: moving objects, noise-making things, and things that could be prey, a friend, or a danger.&lt;/p&gt;

&lt;p&gt;His cat brain just works this way. This interest can be extended through association. For example, I taught him the question &lt;em&gt;"Acıktın mı?"&lt;/em&gt; (Are you hungry?), and he recognizes it. I trained him to respond affirmatively because he knows food follows. But his focus only extends so far, always depending on his basic desires.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flfs5t70ncb7uujrtezz4.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flfs5t70ncb7uujrtezz4.jpeg" alt="My cat sniffing meat on the dining table" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Humans work the same way. We need developed curiosity to care about certain things. My mom doesn't look into a car's design or performance—she just wants to get from point A to B. My brother, however, knows many brands and how they perform differently because he is interested in the joy of driving.&lt;/p&gt;

&lt;p&gt;When I see a website or an app, I see many exciting details. When I show them to non-designers, they often can't say more than "it looks nice" or "the colors are good." They don't care about the details that I recognize separately. Instead, they look for the general feel they get from the design.&lt;/p&gt;

&lt;p&gt;A viewer on a landing page looks for things that interest them: "What is this about?", "What are the benefits?", or "How does this make me feel?" The brain rapidly shifts focus. If they stay within one focus and keep seeing related things, they remain engaged more easily.&lt;/p&gt;

&lt;p&gt;Everyone looks for specific things and ignores the rest, also at varying levels. Some interests can be 1/10 importance while some are 9/10. Catching a viewer's attention requires knowing how that attention flows. A designer’s eye must constantly look for the same things the visitor seeks.&lt;/p&gt;

&lt;p&gt;Because of this, identifying all the things the user looks for — simultaneously and effectively — becomes the essential skill a designer must master to build products that truly achieve success.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Further Reading / Relevant Concepts:&lt;/strong&gt; If you're interested in the psychology behind this, these ideas are also explored in &lt;strong&gt;Gestalt Principles&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Multistability:&lt;/strong&gt; The mind switches between different interpretations of the same image.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Continuity:&lt;/strong&gt; Our eyes naturally follow the smoothest path or line.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Closure:&lt;/strong&gt; The brain fills in gaps to perceive a complete shape.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>design</category>
      <category>uxdesign</category>
      <category>behavior</category>
    </item>
  </channel>
</rss>
