Ivan Akulov
+ Your AuthorsArchive @iamakulov JS developer, web performance consultant. GDE. Running 3perf.com/. Opinions are my own. 🏳️‍🌈 He/him Jun. 24, 2020 12 min read

Okay, let’s do a thing!

One like of this tweet = one web perf tip. Loading perf, runtime perf, whatever ⬇

1) If you do a perf recording of a React app (in the dev mode), the perf trace will include the Timings section.

In Timings, you can see what components were rendered, and when. Useful to debug unnecessary renders:

(Unfortunately, React is removing that due to complexity:  https://github.com/facebook/react/pull/18417 . But it’s still available in the most part of React 16.x)

2) Have a static site? Serving fonts from your own server?

Subset these fonts to characters that are actually used on your pages. This will help to load fonts faster.

You can easily do this at the build time using
 https://www.npmjs.com/package/subfont  or

3) Want to measure how much Google Analytics or other third parties affect your site’s speed?

That’s easy to do with Chrome DevTools.

Go to Network → Sort by domain → Right-click each third-party → Select "Block request domain".

Then, just run a Lighthouse audit and compare

4) Use Gatsby and have a mostly static site? A great way to make it faster is to remove Gatsby’s JavaScript from all static pages.

You can do this with gatsby-plugin-no-javascript:

5) If you’re using Cloudflare, for $20/mo, you can compress all your images automatically. And even convert them to webp if the browser supports that.

Just enable the Polish option:  https://support.cloudflare.com/hc/en-us/articles/360000607372-Using-Cloudflare-Polish-to-compress-images 

6) Another way to load your images faster is to use image-webpack-loader.

Plug this loader in front of url-loader or file-loader, and it will compress and optimize your images as needed ↓


7) *Yet another* way to speed up your images is to serve smaller pictures on smaller screens. There’s no need to load a 4K image for an iPhone SE screen, right?

For this, use <img srcset> and webpack’s responsive-loader:

8) Okay, let’s talk DevTools.

If your app lags when you click something, it’s likely doing too much work.

Sometimes, this work is "repainting too much on the screen". The easy way to debug unnecessary repaints is More tools → Rendering → Paint Flashing:

9) React DevTools have a similar setting that highlights all component renders.

Go to React DevTools settings and check "Highlight updates...". Now, whenever you do something, every component that re-renders will flash for a bit.

Super useful for debugging unnecessary rerenders

10) Want to see if you’re doing code splitting well enough?

Go to your app. Then, open DevTools → Ctrl/⌘+P → Coverage → "Start instrumenting...".

You’ll see how much of your CSS and JS has been actually used for rendering the page:

11) Courtesy of @tkadlec:

To check whether any of your resources are missing gzip/Brotli compression, type `-has-response-header: Content-Encoding` into the filter in the Network panel:

12) If you’re preloading fonts, make sure you use the crossorigin="anonymous" attribute.

Due to CORS trickery, without that attribute, preloaded fonts will be ignored ( https://github.com/w3c/preload/issues/32 )

(Shameless plug: if you’d love to learn more of these tips & how they apply to your app or site, see  https://3perf.com/consulting/ )

13) Have a static site? Want to make navigation faster? Add  http://getquick.link  or  http://instantclick.io .

— instant-click preloads links when the visitor hovers them (this gives a 100-300 ms head start)
— quicklink goes further and preloads all links within the viewport

14) Use Bootstrap or another CSS framework? It’s likely you’re serving a lot of CSS that you don’t use.

Add purgecss-webpack-plugin to your webpack config to remove unused classes ↓


15) A great way to speed up custom fonts is to use `font-display`.

By default, any text that uses custom fonts isn’t visible until these fonts load (or up to 3s). This is a subpar UX.

You can change that by setting the `font-display` rule in your CSS:  https://font-display.glitch.me/ 

16) Google Fonts have been supporting `font-display` for a year.

But if your site is older, it’s likely you don’t have it enabled.

Make sure you Google Fonts URL has the `&display=swap` (or another value) parameter to get font-display benefits:

17) Using Lodash? Make sure your Babel config has babel-plugin-lodash.

babel-plugin-lodash transforms your Lodash imports to make sure you’re only bundling methods that you actually use (= 10-20 functions instead of 300).

18) Using Lodash? Try aliasing `lodash-es` to `lodash` (or vice versa). E.g., with webpack ↓

A common issue in bundles I’ve seen is different dependencies using different Lodash versions. This leads to Lodash being bundled multiple times.

Using Moment.js? Try replacing it with Day.js.

Day.js has the similar API, also supports locales, but is orders of magnitude smaller:  https://github.com/iamkun/dayjs 

20) Using React? Try replacing it with preact + preact-compat.

In a lot of bundles I’ve seen, react-dom is the single largest dependency. Just by removing it, you can reduce your load time quite significantly.

I’ve got to admit, I’ve been doubtful about this optimization for a while – mostly due to compatibility concerns. Like, what if I replace React with Preact, and it breaks something?

But then, I did this at  http://3perf.com . And it was seamless. I have yet to see any bugs.

21) /*#__PURE__*/

This is my favorite conference trick.

If you have a function that you
— call once,
— store its result in a variable,
— and then don’t use that variable –
tree-shaking will remove the variable, but *not* the function.

That’s because the function can have any side effects (e.g. what if it sends something to the server?), and removing it might break the app.

However, if the function doesn’t have side effects, this’d mean it’s kept in the bundle unnecessarily.

To remove such function when its result is not used, prepend the function call with /*#__PURE__*/:

This is supported by Uglify, Terser, and a few other tools – and it tells them that it’s safe to remove `getTodaysFavoriteColor()` if `color` is not used.

22) BTW, that’s also why you should use babel-plugin-styled-components with styled-components (and similar plugins with other libs).

These plugins prepend /*#__PURE__*/ in front of CSS-in-JS declarations. Without them, unused CSS rules won’t be deleted from the bundle.

23) If you use webpack with HTMLWebpackPlugin, make sure to enable `optimization.splitChunks: 'all'`:  https://webpack.js.org/plugins/split-chunks-plugin/ 

This would make webpack automatically code-split your entry bundles for better caching.

(This is useful without HTMLWebpackPlugin as well. But it’s super convenient with it because HTMLWebpackPlugin takes care of including all necessary bundles.)

24) Also, set `optimization.runtimeChunk: true`:  https://webpack.js.org/configuration/optimization/#optimizationruntimechunk 

This would move webpack’s runtime into a separate chunk – and would also improve caching.

25) *If* you’re doing manual code splitting (= import() or multiple entries), *do not* code-split node_modules into a vendor bundle. I.e., *do not* do this ↓

Yes, this example is present it docs. But with multiple chunks, it’s harmful.

If any of your chunks uses a large dependency (e.g., moment), this dep would be moved into the vendor bundle. And *all* pages of your app will have to load it.

Instead, code-split common modules:

Here’s a great case study about Next.js and Gastby covering this topic:  https://web.dev/granular-chunking-nextjs/ 

26) If you’re inlining svgs into the bundle, use svg-url-loader:  https://www.npmjs.com/package/svg-url-loader 

base64-encoded resources are, on average, 37% larger than original assets due to limited alphabet.

svg-url-loader encodes svgs using URL encoding, so svgs don’t suffer from that:

27) But, also: if you’re inlining svgs into the bundle, run webpack-bundle-analyzer and confirm you’re not inlining *too many* of them.

This is frequent with svg icons. Each icon might be small (1-2KB), but when there’re 200 icons, suddenly, that affects the bundle a lot.

28) Using Google Fonts and HTMLWebpackPlugin? Self-host these fonts with google-fonts-webpack-plugin.

The plugin downloads font files – so you can serve them from your server.

This makes fonts load faster – as the browser doesn’t have to set up a new connection to Google Fonts.

29) Want to preload all your webpack assets ahead of time? Or even make the app work offline? Use workbox-webpack-plugin:  https://www.npmjs.com/package/workbox-webpack-plugin 

The plugin will generate a service worker – which, with a couple of flags, can do any of these things.

30) While we’re on the same page: workbox is 🔥

If you wanted to add a service worker to your app but were always pushed away by the complexity, check out Google’s workbox library:  https://developers.google.com/web/tools/workbox 

It puts all common SV usage patterns behind a simple interface.

31) Want to preload webpack assets but not ready for a full-blown service worker? Then try preload-webpack-plugin:  https://www.npmjs.com/package/preload-webpack-plugin 

This plugin works with HTMLWebpackPlugin and generates <link rel="preload/prefetch"> for all JS chunks:

32) BTW, if you’re using webpack, it’s likely all your JS/CSS/images/etc have a hash in their name. E.g.:


You can improve caching of such assets with the `Cache-Control: immutable` header. More info:  https://bitsup.blogspot.com/2016/05/cache-control-immutable.html 

33) And here’s how you typically do caching, anyway:

34) Your CSS consists of two parts:
— what’s needed for initial rendering
— and what’s not

The second part is typically larger – and it’s not needed for the first render! This means it just adds a delay. To split these parts & remove the delay, use  https://github.com/addyosmani/critical 

This approach is called "Critical CSS". More about it:  https://3perf.com/talks/web-perf-101/#critical-css 

35) CSS-in-JS has its drawbacks, but one of its benefits is that it typically supports Critical CSS out of the box. (CSS modules is a notable exception.)

So if you’re using styled-components (or smth similar), there’s no need to use `critical` on top of that.

36) If you use styled-components or emotion, try replacing them with linaria:  https://www.npmjs.com/package/linaria 

Both styled-component and emotion have a runtime, and that brings runtime performance costs ( https://calendar.perfplanet.com/2019/the-unseen-performance-costs-of-css-in-js-in-react-apps/ ).

Linaria is a 0-runtime alternative with a similar API:

37) Defer your third-parties.

Google Analytics, Intercom, and other third party scripts steal bandwidth and CPU time from your app. E.g., here’s a great example:  https://3perf.com/blog/notion/#defer-third-parties 

To make sure they don’t affect your loading time, don’t load them till your app initializes:

38) Or just wrap your third party loading code with `setTimeout`. Almost as good and super simple:

199 likes, halp, how do I come up with enough tips

39) Okay, let’s talk about tooling.

Almost everyone knows about Lighthouse. But not everyone knows that you can run Lighthouse from cli:  https://www.npmjs.com/package/lighthouse 

Useful if you need to automate some tests!

40) Lighthouse CLI also supports a bunch of advanced options not available in DevTools – like custom throttling settings, or extra HTTP headers:

41) Another great tool is WebPageTest:  https://webpagetest.org/ 

WebPageTest is the most advanced perf tool I know. It has a ton of use cases – but one of my favorites is testing perf from real devices and various locations.

iPhone 6 in the US? Yes. Firefox in India? Why not.

42) Webpack has a whole ecosystem of tools around it.

`duplicate-package-checker-webpack-plugin` warns if you have multiple versions of the same library bundled (which is super common with core-js):


43) bundle-buddy shows which modules are duplicated across your chunks. Use it to fine-tune code splitting:  https://www.npmjs.com/package/bundle-buddy 

44) With  http://webpack.github.io/analyse/ , you can figure out why a specific module is included into the bundle.

Useful if you see something large in the webpack-bundle-analyzer report, and you aren’t sure why it’s there.

45) source-map-explorer build a map of modules and dependencies based on a source map:  https://www.npmjs.com/package/source-map-explorer 

Unlike webpack-bundle-analyzer, it only needs a source map to run. Useful if you can’t edit the webpack config (e.g. with create-react-app).

46) bundle-wizard also builds a map of dependencies – but for the whole page:  https://www.npmjs.com/package/bundle-wizard 

47) Okay, enough tools.

HTTP/2 is fast, in part, because it sends all assets over a single connection. But – sometimes that breaks.

E.g., if you misconfigure the crossorigin attribute, the browser would be forced to open a new connection.

To check whether all requests use a single HTTP/2 connection, or something’s misconfigured, enable the "Connection ID" column in DevTools → Network.

E.g., here, all requests share the same connection (286) – except manifest.json, which opens a separate one (451):

48) Use  http://polyfill.io  to reduce the amount of polyfills you’re serving.

 http://polyfill.io  inspects the User-Agent header and serves polyfills targeted specifically at the browser. So modern Chrome users receive nothing, and IE 11 users get everything.

49) Or: if you use babel-preset-env and Core.js 3+, enable `useBuiltIns: "usage"`.

This will bundle polyfills that you’d actually use and need:  https://babeljs.io/docs/en/babel-preset-env#usebuiltins-usage 

50) If you have any `scroll` or `touch*` listeners, make sure to pass `passive: true` to addEventListener.

This tells the browser you’re not planning to call event.preventDefault() inside, so it can optimize the way it handles these events.


51) One common runtime perf issue is when the code reads and sets properties like `width` or `offset*` several times in a row.

The problem is, every time you change and then read a width or something, the browser has to recalculate the layout:

And if you do this multiple times in a row, this easily gets slow.

Here’s the full list of properties that do this:  https://gist.github.com/paulirish/5d52fb081b3570c81e3a 

And here’s how to rewrite the code above so there’s no issue:

52) Debugging a React app? Not sure why a component gets rerendered?

1. Go to React DevTools → Profiler → Settings. Enable "Record why each component rendered"
2. Start the recording, do whatever you did, stop the recording
3. Click on the component in the perf trace


53) Debugging a React app? Not sure why a component gets rerendered?

Another way to figure out why a component re-renders is to use why-did-you-render:  https://github.com/welldone-software/why-did-you-render 

Demo pic from the docs:

54) Sometimes, you have a forced layout/style recalculation – but can’t remove it (eg because it’s caused by a third-party lib).

In this case, try limiting the scope of the recalc using `contain`:

.recaculated-elem { contain: content }

This can make the recalc much cheaper!

The `contain` CSS property tells the browser that the element is isolated from the surrounding document.

So if something changes inside the element, the browser could avoid recalculate just this element instead of the whole document.


You can follow @iamakulov.


Tip: mention @threader_app on a Twitter thread with the keyword “compile” to get a link to it.

Threader is an independent, ad-free project created by two developers. Our iOS Twitter client was featured as an App of the Day by Apple. Sign up today to compile, bookmark and archive your favorite threads.

Follow Threader