justMakeItFaster!

React initial rendering performance

React rendering performance

Luca Colonnello

Full Stack Engineer / Tech lead

@LucaColonnello

🇮🇹🇬🇧

Why this talk?

Raise awareness about the
impact of modern front-end development techniques on speed and UX

Give some tips on how to analyse and solve some of these perf issues, helping you improve the quality of what you're building

Sit back and relax

we're about to embark on a journey...

@LucaColonnello

Close your eyes

You're now a Performance Engineer

working for a company called justPay!

You are assigned to a new project...

Sit back and relax

we're about to embark on a journey...

@LucaColonnello

Close your eyes

You're now a Performance Engineer

working for a company called justPay!

You are assigned to a new project...

justMakeItFaster!

The CEO wants to open the business to new markets, so we need to make our app faster in low end devices.

justPay!
Home page

@LucaColonnello

@LucaColonnello

Improve low end devices rendering and interactivity time

The goal

@LucaColonnello

Reality check

In March 2019

Of this 76.5%, 20.8% were high end devices, up from 15% in March 2018

76.5% of active smartphones worldwide were Android

🇺🇸 48.3%

🇮🇳 95.9%

@LucaColonnello

Connectivity

First things first

a small framework to handle performance

@LucaColonnello

Measure

Analyse

Improve

@LucaColonnello

Measuring initial render

@LucaColonnello

@LucaColonnello

What happens when I hit https://www.justpay.com/ for the first time?

@LucaColonnello

@LucaColonnello

FONT

IMG

JS

CSS

HTML

Layout

Paint

JS Evaluation

Render of content 👇

Analysis

@LucaColonnello

  • Rendering happens too late, after JS runs
  • Our JS execution is very large
  • The Layout and Paint happens after JS runs

Why isn't the browser rendering while executing JS?

@LucaColonnello

💡Knowledge

@LucaColonnello

JS runs in tasks

Task

call stack

document.body.appendChild(...)

Main thread blocking

💡Knowledge

@LucaColonnello

Layout and Paint don't run while JS is running
(simplified, there are edge cases for layout)

Task

Every task in the browser blocks others until is finished, the main thread is like a scheduled queue

💡Knowledge

@LucaColonnello

An example of this: rendering many charts

showLoadingSpinner();

const data = calculateDataForAllCharts();
renderAllCharts(data);
// 👆above 2 lines take 1 second

hideLoadingSpinner();

Task

Expectation vs Reality

🕓

📊

📊

@LucaColonnello

Although your JS might try to render using DOM APIs, Paint happens after the current JS task is finished

Therefore, the performance of your React app affects WHEN the user is going to see something in screen.

@LucaColonnello

This is a user waiting for your JS...

How can we improve the rendering time?

@LucaColonnello

@LucaColonnello

How can we improve the rendering time?

Server Side Rendering

@LucaColonnello

FONT

IMG

JS

CSS

HTML

Layout

Paint

JS Evaluation

Render of content 👇

@LucaColonnello

Improve

@LucaColonnello

Improve

Our users can now see something in the screen way earlier and benefit from the content.

The page feels faster as it almost instantly renders straight after downloading the HTML.

@LucaColonnello

Measuring interactivity

@LucaColonnello

Measuring interactivity

How does our JS affect users trying to interact with the elements on the page after rendering?

@LucaColonnello

Let's look at justPay! home page features

@LucaColonnello

Header

CategoriesMenu

UserQuickLinks

TopDeals

NavModal

@LucaColonnello

BasketSummary

BasketModal

@LucaColonnello

SearchBar

SearchResults

SearchModal

@LucaColonnello

TopGadgets

TopGadgetPopup

@LucaColonnello

TwitterReviews

PeopleFavouritesList

@LucaColonnello

TopCategoryList

@LucaColonnello

Footer

@LucaColonnello

FONT

IMG

JS

CSS

HTML

Layout

Paint

JS Evaluation

Hydrating...

Render of content 👇

👆

👇

Interaction

👇

Handler

😡 Delay

@LucaColonnello

Analysis

  • Interactions are blocked by our JS until hydration is done
  • If the user interacts while hydration is running, there will be a delay
  • Our hydration step is too big

@LucaColonnello

> 300ms delay is perceived by users as slow, janky

Task

High Input Delay

👇 user interaction

> 300ms

👇 handler

@LucaColonnello

Why is our initial JS execution this big?

@LucaColonnello

💡Knowledge

Our hydration is made of 2 main elements

Task

webpack_require

ReactDOM.hydrate

@LucaColonnello

💡Knowledge

webpack_require

import React from 'react';
import { hydrate } from 'react-dom';

import App from './App';

hydrate(<App />, document.getElementById('#main'));

ReactDOM.hydrate

webpck_require is used to run every module (and its modules) statically imported in the bundle.

This operation is blocking.

When running ReactDOM.hydrate, React needs to run every component our App is mounting.

This operation is blocking.

@LucaColonnello

The amount of modules we import in our bundle has a cost in evaluation, not just in size.

Hydrating our React App has a linear cost, the more components we have, the more it will cost.

By improving the Hydration time, we are freeing the main thread earlier, meaning our users can interact with the element in our page earlier.

@LucaColonnello

How can we improve the Hydration time?

@LucaColonnello

How can we improve the Hydration time?

Removing unnecessary work at first render

@LucaColonnello

App

Footer

TwitterReviews

TopDeals

TopDealsItem

TopCategoryList

PeopleFavouritesList

TopGadgets

Header

SearchModal

BasketModal

NavModal

CategoriesMenu

UserQuickLinks

Good

Too High

Self render time (not including children)

@LucaColonnello

Only 1 component seem to be heavy.
The rest is quite fast individually.

It's the amount of components which makes it slower.

When hydrating or rendering for the first time, React has to go through all your components.

@LucaColonnello

Lazy Hydration

@LucaColonnello

Task

Task

Task

Task

Task

< 300ms

user interaction 👇

Hydrated rest subsequently, when visible

Hydrate most important things first

👇 event handler

by breaking it down into smaller tasks

we release the CPU earlier, hence less input delay

@LucaColonnello

Hydrate as you go, when needed

When elements are in viewport

Hydrate what's needed, do not hydrate static content

@LucaColonnello

👨‍💻Demo

@LucaColonnello

const App = () => (
  <Header />
  <TopDeals />
  <TopGadgets />
  <TwitterReviews />
  <PeopleFavouritesList />
  <TopCategoryList />
  <Footer />
);
const App = () => (
  <Header />
  <TopDeals />

  <LazyHydrate whenVisible>
    <TopGadgets />
  </LazyHydrate>

  <LazyHydrate ssrOnly>
    <TwitterReviews />
    <PeopleFavouritesList />
    <TopCategoryList />
  </LazyHydrate>

  <LazyHydrate whenVisible>
    <Footer />
  </LazyHydrate>
);

From

To

@LucaColonnello

App

TopDeals

TopDealsItem

Header

SearchModal

BasketModal

NavModal

CategoriesMenu

UserQuickLinks

Good

Too High

Self render time (not including children)

@LucaColonnello

~40% less spent on hydration

@LucaColonnello

Let's tackle the heavier components

@LucaColonnello

Let's tackle the heavier components

NavModal

CategoriesMenu

Time taken to render just the component,
not including its children

@LucaColonnello

const CategoriesMenu = ({ allMenuItems, categories }) => {
  // expensive denormalisation of data
  const categoriesMenuData = buildCategoriesMenuData(
    allMenuItems,
    categories
  );

  return (<AccordionMenu data={categoriesMenuData} />);
};
// creating category -> menu items out of a flat list
/*
   {
     category: String,
     links: [
        {
          label: String,
          href: String,
        }
     ]
   }
*/

const buildCategoriesMenuData = (allMenuItems, categories) =>
  categories.map(
    category => ({
      category: category.name,
      links: allMenuItems
      	       .filter(menuItem => menuItem.category === category.id)
	           .map(({ label, href }) => ({ label, href }))
    })
  );

allMenuItems[{...}]

0:

category (Int)

label (String)

href (String)

categories[{...}]

0:

id (Int)

name (String)

@LucaColonnello

const CategoriesMenu = ({ categoriesMenuData }) => (
  <AccordionMenu data={categoriesMenuData} />
);

Hoist to the server, you could have a presentational API (i.e. GraphQL)

@LucaColonnello

Calculate during SSR, skip during hydration

const CategoriesMenu = ({ allMenuItems, categories }) => {
  const categoriesMenuData = useSSRMemo(
    'categoriesMenuData',
    () => buildCategoriesMenuData(
      allMenuItems,
      categories,
    ),
    [allMenuItems, categories]
  );

  return (<AccordionMenu data={categoriesMenuData} />);
};

useSSRMemo could use Context to collect the values in the server.

The server could then print the JSON data to be used by the client to skip the calculation.

@LucaColonnello

App

TopDeals

TopDealsItem

Header

SearchModal

BasketModal

NavModal

CategoriesMenu

UserQuickLinks

Good

Too High

Self render time (not including children)

@LucaColonnello

another ~10% less spent on hydration

@LucaColonnello

Improve

@LucaColonnello

Congratulations!
You're customers are now happy!

@LucaColonnello

Let's recap...

Use SSR to deliver content to users earlier

Don't hydrate more than necessary

Hoist as much as possible to the server

@LucaColonnello

P.S.

React will possibly make some of this easier with Progressive and Selective Hydration

You can only optimise so much, so give your app a budget for hydration and have discussions with product about the impact of new features

@LucaColonnello

Special thanks...
🙏

Questions?

Thank you for listening
🙏

React initial rendering performance

By Luca Colonnello

React initial rendering performance

  • 436