Optimizing for Modern Web šŸš€

Hasan GenƧ
Trendyol Tech
Published in
5 min readJun 16, 2021
High lighthouse score is not the goal but just the result of your work.

Optimization is a continuous process, not a one-time checklist. You just need to figure out why and what to optimize. Therefore, measuring and analyzing play a crucial role in exposing the barriers that affect the performance of your awesome application.

This post is about how we achieved significant performance gains by adapting our applications to the modern Web features.

Since Web is evolving continuously, new opportunities coming up to make our apps more performant and user-friendly. These are the steps of our optimization process: Measure, Analyze, Optimize, Monitor.

Note: You shouldnā€™t jump over to the optimization step without making an analysis, because it may not be what your application needs to improve and may cause a waste of time.

šŸ“ˆ Measure & Analyze

Letā€™s start with lighthouse audits because this will give us a baseline on where to look afterward. Look at the below lighthouse result, there is a metric that differs from the others. Speed index is calling us for help.

Speed Index shows how quickly the contents of a page are visibly populated. And optimizing it improves our overall performance score.

JavaScript execution time may be a huge bottleneck to the speed index. More bytes cause more network, parse & compile, execution, and memory costs.

Before making a decision, letā€™s measure a few more metrics.

We can get the network trace of our application by recording a JavaScript profile from DevTools. As the below image shows, there is a long-running task in the time frame which loads the main JavaScript file for the page.

Network trace (slow 3G)

Letā€™s capture the code usage coverage of that file:

It seems like ~53% of the code in this script is never used. By inspecting the contents of the file we can see that the code has ES5 syntax (lots of var, function , Object.assign statements).

JS file with ES5 syntax

We know that ES6 is supported by all modern browsers, so thereā€™s no reason not to use it. Also, there is just one JavaScript file that hydrates all the content of the page. We can use code splitting according to usage and cost.

āš” Optimize

After measuring and making analysis we can tell that minimizing the large files can make difference in terms of network & Javascript execution time. We can ship ES6 code with code splitting and still support the legacy browsers.

We recently switched from webpack to ViteJS because of Viteā€™s out-of-the-box features like auto/manual chunking, ESM support, legacy plugin, HMR, built-in typescript, react, CSS modules support, esbuild, etc.

By default, you donā€™t need to have a custom ViteJS build config. But we needed to customize it because of some cases, and I think it may be helpful to share that.

You may have some external scripts/modules loaded to your page and you donā€™t want to include them in your bundle. Therefore, you need to define your externals in the vite.config.ts as below.

We specify externals as

  • rollupOptions.external for the ES modules
  • commonjsOptions.ignore for the CJS packages
  • commonjsOptions.esmExternals for the ESM & CJS mixed packages

This configuration might be enough for you but, Vite has known issues in some particular cases (like mixed module formats). If you get some errors about the external dependencies, you can talk with the author or if you are the author you can publish the ESM version of that package.

We had a 69.1 kB bundle before the optimization.

old webpack es5 output

Now, we have 16.9 kB main.js and 5.9 kB vendor chunks (auto-generated by Vite).

vite es6 output

Letā€™s look at the network trace now. 25.41 s long task is now just 5.43 s long.

Network trace (slow 3G)

Also, the unused code percentage reduced from 52.9% to 18.7%.

And, these changes reflected in our overall performance score.

Note: We didnā€™t focus on just one metric. We ship less code to the browser so, it speeds up the JavaScript execution time and increases our overall performance score.

Supporting Legacy (EOL) Browsers šŸ”«

Serving modern code minimized our bundle size by ~%70. But modern code only works at modern browsers and we still need to support old browsers like IE ā‰¤ 11, Safari ā‰¤ 9 which don't support ES modules and are still used by significant numbers of users. (Although ~2% may seem small, if you have millions of users, supporting old browsers can result in significant revenue).

Usage share of web browsers in November 2020 according to StatCounter

We use differential serving approach to support both modern and legacy browsers. Usingnomodule module content attributes enables to load & execute different scripts according to the user agent.

Vite has an official legacy plugin named @vitejs/plugin-legacy for differential serving. But I forked this plugin because of some issues the official plugin has.

Usage of the plugin is pretty simple. You just pass your target like browserlist and specify some polyfill if you need to. Additionally, you have to use regenerator-runtime to support IE 11.

Vite config that generates IE11 compatible output

It creates ES5 output using babel which uses SystemJS for module imports. You can add them tonomodule scripts.

šŸ‘€ Monitor

We shouldnā€™t leave without monitoring changes. Use tools like lighthouse-ci, lighthouse, web-vitals to gather data and monitor it periodically. We saw significant changes in our charts as below. Also, make sure that your error rate is not affected by these changes.

daily web perf metrics

Further ā€œPossibleā€ Optimization Ideas:

Thank you for reading, bye āœ‹

--

--