Getting Content Painted under 2 seconds on the Mobile Web
By: Jacky Efendi
Tags: JavaScript, Frontend, Web Development, Svelte, Web Performance

Discovery: Our JS seems to be blocking LCP

After some investigation (mostly using the devtools), we found that our JS seemed to be blocking the LCP of our page.

Notice that in the image above, LCP only happens after some JavaScript execution is finished. Even though the network request for the image has already finished as denoted by the first vertical orange line. This is not something we expected to see. Since we are doing server-side rendering, the image should already exist in the initial HTML document and should be able to be painted without depending on JavaScript.

We then thought that we needed to find answers to these 2 questions:

Did our JS resize the LCP element? Was the LCP element removed and re-inserted into the DOM?

Did our JS resize the LCP element?

If an LCP element was resized, the previous collected timings would no longer be considered as the LCP. We used ResizeObserver to detect any resizes that happens on our LCP element.

Example of using ResizeObserver

Unfortunately, this wasn’t the culprit. We didn’t find any resizing happening to the LCP element.

Was the LCP element removed and re-inserted into the DOM?

If an LCP element is removed from the DOM, it will no longer be considered as an LCP element (this might change in the future though!), thus invalidating the previously collected LCP timings. To check for this, we relied on lesser-known DOM event, namely DOMNodeInserted and DOMNodeRemoved .

After testing this, we found that this was indeed the case! But why? Why were we removing the element and re-insert it into the DOM?

Svelte Hydration

Apparently, this is a known issue with how Svelte does hydration. Svelte remove nodes and re-insert them to the DOM. There is an issue on Svelte repo specifically discussing about this.

An issue on Svelte repo discussing about hydration

Seeing how this is an issue on the framework level, we weren’t sure on how to go around this. We tried monkey-patching Node.prototype methods. It worked and improved our LCP, but it caused huge performance issues. Forking Svelte ourselves seemed to be a task that requires huge effort. Fortunately, there is a fork of svelte that specifically works around the hydration mechanism.

We used it in our project by updating our package.json dependencies.

Using this forked version of Svelte, we saw that our LCP now happens even before the JS execution finishes, which is what we wanted to see!

Result

The LCP element is no longer depending on the JS execution, the LCP can happen much earlier in the page lifecycle! Visually, there are no noticeable changes though.

This final change finally brought us over to the green zone!

We are in the green!

If you are reading this, please note that we do not recommend using an unofficial version of svelte on your production site. As for our case, we did our tests and confirmed everything was working according to our expectation. We felt the tradeoff was acceptable and went along with it.

Do not just trust what you read from tech articles, always do your own assessment as well!