Skip to content

Latest commit

 

History

History
398 lines (322 loc) · 13.9 KB

README.md

File metadata and controls

398 lines (322 loc) · 13.9 KB

Performance

Table of contents

  1. Tools for performance testing
  2. Bundle optimization
  3. Dynamic reducers
  4. Code performance improvements

1. Tools for performance testing

  • lighthouse
  • bundle-analyzer

1.1. Lighthouse

Integrated into chrome browsers, tool for running performance checks on websites - gives detailed report about required optimizations. More info: https://developers.google.com/web/tools/lighthouse/

1.2. Bundle Analyzer

Shows what components are inside each Reacts generated chunk.

1.2.1. Enabling bundle-analyzer

Add @scandipwa/bundle-analyzer into package.json file:

{
    ...
    "dependencies": {
        "@scandipwa/m2-theme": "^0.1.4",
        "@scandipwa/bundle-analyzer": "0.0.7",
        ...
    },
    "scandipwa": {
        ...
        "extensions": {
            "@scandipwa/m2-theme": true,
            "@scandipwa/bundle-analyzer": true,
            ...
        },
       ...
    },
    ...
}

After enabling extension, the bundle analyzer will automatically start together with PWA app. It will run on the next available port (Default: 8002).

(Bundle analyzer will show all chunks, to know what chunks are loaded on each page you can use web browsers developer tools - Debugger or Network)


2. Bundle optimization

Bundle optimization - process in witch we reduce chunks size to improve page loading speed, by joining only those components into chunks that are required on request, instead of loading all of them at once.

2.1. Splitting chunks/bundle

Splitting chunk into smaller ones can be done in React with two commands:

  • lazy - imports component async, imports component when it is called for first time:
...
import { lazy } from 'react';
...
export const MyAccountAddressBook = lazy(() => import(
    'Component/MyAccountAddressBook'
));

// Or specify chunk name via 'webpackChunkName'
export const MyAccountAddressBook = lazy(() => import(
    /* webpackMode: "lazy", webpackChunkName: "account-address" */
    'Component/MyAccountAddressBook'
));
...
  • Suspense - used to output fallback component while main component is still loading in:
...
import { Suspense } from 'react';
...
<Suspense fallback={ <Loader /> }>
  <MyAccountAddressBook />
</Suspense>

2.2. Splitting

Elements that should be moved to separate chunks for better performance:

  • Pages - Each page should be moved to separate chunk. (As we shouldn't be loading components from different pages into all pages - main chunk)

  • Sections - You can split pages into smaller chunks to prioritize loading of one element before other.

  • Tabs - if page/section contains tab switching (example: MyAccount, ProductDetails ...) then each tab should be moved to separate chunk, thus loading necessary data only on request.

  • Widgets - as widgets can be placed into multiple places, then for best performance each widget should be placed ether in shared widget chunk (widget) or in separate chunk with naming following: widget-{ name of widget }, thus loading only necessary components;

  • Special case utility - utility that is uesed only in few other chunks can be moved out to seperate chunk.

  • External components - for simpler use external components can be moved to new component that utilizes lazy loading / suspense, thus allowing to use this component from one chunk.

import { Suspense } from 'react';

// Component/ExternalComponent
export const ExternalComponent = lazy(() => import(
    /* webpackChunkName: "external-{name}" */
    'package'
));

export const renderWithDom(dom)  => {
  <Suspense fallback={ <Loader /> }>
    <ExternalComponent dom>
  </Suspense>
};

export const renderWithText(text)  => {
  <Suspense fallback={ <Loader /> }>
    <ExternalComponent text>
  </Suspense>
};
  • Shared components outside the main chunk - import structure should be analyzed to see what component is used where, thus allowing to move chunks that are shared only between two or three components into one new chunk.

2.3. Import optimization

2.3.1. Constant imports

When importing constants into a component the whole file will be added into chunk, thus you should be sure that a specific constant is present in the correct file.

For example when possible you should use .config.js files to store constants and then import from that instead of splitting them into .container.js and .config.js files, as .config.js file by itself is smaller than .container.js it will also remove whole file from chunk.

2.4. Small chunk merging

You should investigate "nameless" chunks (chunks that are automatically generated by webpack containing numeric names) and search for chunks with similar import data. Chunk merging can be done in one of the two ways:

  • Investigate similar chunks and based on content find the import that is responsible for it in the code base. Then with react lazy loading and webpack chunk naming join them.
  • Configure webpack so that smaller packages are joined together.

2.5. Webpack optimization

Sample of webpack configuration to optimize chunks

optimization: {
    runtimeChunk: 'single',
    splitChunks: {
      chunks: 'all',
      maxInitialRequests: Infinity,
      minSize: 30,
      maxSize: 200,
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name(module) {
            // get the name. E.g. node_modules/packageName/not/this/part.js
            // or node_modules/packageName
            const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1];

            // npm package names are URL-safe, but some servers don't like @ symbols
            return `npm.${packageName.replace('@', '')}`;
          },
        },
      },
    },
}

3. Dynamic reducers

To keep up with principle of only loading what’s needed, we have created new solution for importing reducers into project.

Reducers in project should be splitted into two categories:

  • Static - widely used between multiple components
  • Dynamic - used for specific cases, thus instead of loading all reducers in all pages you can split them so that only necessary ones are loaded via new command withReducers
...
import { withReducers } from 'Util/DynamicReducer';
import CategoryReducer from 'Store/Category/Category.reducer';
...
export default withReducers({
    CategoryReducer
})(connect(mapStateToProps, mapDispatchToProps)(CategoryPageContainer));

by using command withReducers the specific reducer is loaded async only when required.

(If reducer is loaded in main component then all exiting / rendered componnent will also have access to this reducer, so best practice should be loading reducers in Page containers)


4. Code performance improvements

4.1. Reduce component rendering count

Reducing components rendering count can improve pages first time load score. React offers hook shouldComponentUpdate.

We can look at our current and new props & state and make a choice if we should move on. For example, some components may need to update only after main prop or state has changed and can ignore re-rendering from other changes.

  function shouldComponentUpdate(nextProps, nextState) {
      const { mainProp } = this.props;
      const { mainProp = nextMainProp } = nextProps;

      return mainProp !== nextMainProp;
  }

On many cases first time load will execute render method up to eight times, this count can be reduced by performing first time load check together with last updated item check, thus reducing re-rendering count from eight to two.

In many cases all other props and sates are dependent on one specific variable (for example - product), in these cases you can usually perform only specific variable check, although these changes should be tested in-depth, as they can break some functionality.

4.2. Avoid inline functions

Since functions are objects in JavaScript ({} !== {}), the inline function will always fail the prop diff when React does a diff check.

An arrow function will create a new instance of the function on each render if it's used in a JSX property. This might create a lot of work for the garbage collector.

// Bad practice
...
render() {
  return (
    <Example onClick={(e) => { ... }}>
  );
}
...

// Good practice
...
onExampleClick = (example) => {
  ...
}

render() {
  return (
    <Example onClick={this.onExampleClick}>
  );
}
...

4.3. Throttling and debouncing events

Event trigger rate is the number of times an event handler invokes in a given amount of time. In general, mouse clicks have lower event trigger rates compare to scrolling and mouseover. Higher event trigger rates can sometimes crash your application, but it can be controlled.

4.3.1. Throttling

In a nutshell, throttling means delaying function execution. So instead of executing the event handler/function immediately, you’ll be adding a few milliseconds of delay when an event is triggered.

4.3.2. Debouncing

Unlike throttling, debouncing is a technique to prevent the event trigger from being fired too often.

4.4. Avoid async call in componentWillMount

When performing async calls in componentWillMount hook the function will lack access to refs and DOM element, instead of using this hook use alternative that will be fired after render - componentDidMount.

4.5. Avoid Props in States

Avoid setting state values from props in constructor otherwise, you will lose linkage.

4.6. Memorizing React components

By using React.memo, we can store component into memory and on recall will perform a shallow equal comparison of both props and context of the component based on strict equality, and based on that will ether load existing component or repopulate and re-render it.

4.6.1. useMemo hook

Allows you to memoize expensive functions so that you can avoid calling them on every render. You simple pass in a function and an array of inputs and useMemo will only recompute the memoized value when one of the inputs has changed.

import { useMemo } from "react"
...
const test = useMemo(() => expensiveComputation(parameter), [parameters]);

4.7. CSS Animations over JS Animations

There are 3 ways of perfoming animations in web browser (sorted by performance cost):

  1. CSS transitions
  2. CSS animations
  3. JavaScript Most modern browsers are already optimized for handling CSS animation, however js animations aren't, thus requiring creator to optimize them themself.

4.8. Use WebWorkers to reduce main thread load

Web Workers makes it possible to run a script operation in a web application’s background thread, separate from the main execution thread. By performing the laborious processing in a separate thread, the main thread, which is usually the UI, can run without being blocked or slowed down.

In the same execution context, as JavaScript is single threaded, we will need to parallel compute. This can be achieved two ways. The first option is using pseudo-parallelism, which is based on setTimeout function. The second option is to use Web Workers.

(Although most computation / data preparation should be done on the server side (as they can be cached) there may be some exceptions in which moving some tasks to separate "thread" can improve main thread load.)

// Bad practice
function sort (products) {
  for (...)
    for (...) {
      const tmp = product[x];
      product[x] = product[y];
      product[y] = tmp;
    }
}

...

sortProducts = () => {
    const { products } = this.state;

    this.setState({
        products: sort(products)
    });
}

render() {
  const { products } = this.state;

  return (
    <>
      <Button onClick={this.sortProducts} />
      <Products products={products}>
    </>
  )
}

// Good practice
function sort (products) {
  self.addEvenetListener('message', e=> {
    for (...)
      for (...) {
        const tmp = product[x];
        product[x] = product[y];
        product[y] = tmp;
      }
    postMessage(products);
  });
}

...

componentDidMount() {
    this.worker = new Worker('sort.worker.js');
    
    this.worker.addEventListener('message', event => {
        const sortedProducts = event.data;
        this.setState({
            products: sortedProducts
        })
    });
}

sortProducts = () => {
    const { products } = this.state;
    this.worker.postMessage(products);
}

render() {
  const { products } = this.state;

  return (
    <>
      <Button onClick={this.sortProducts} />
      <Products products={products}>
    </>
  )
}

4.9. Lazyloading long lists

When rendering large lists of data, it is recomended that only visible portion of elements is outputed and all other elements are loaded when viewport enters view.

4.10. Optimize Conditional Rendering

// Bad practice
render() {
  if (test === 'test') {
    return (
        <>
          <A />
          <B />
          <C />
        </>
    );
  } else {
    return (
        <>
          <B />
          <C />
        </>
    );
  }
}

// Good practice
render() {
  return (
      <>
        { test === "test" && <A /> }
        <B />
        <C />
      </>
  );
}

In the "Bad practice" example conditional operator and if else condition seems to be fine but it has a performance flaw.

Each time the render function is called and the value toggles between "test" and another value, a different if else statement is executed.

The diffing algorithm will run a check comparing the element type at each position. During the diffing algorithm, it seems that the A is not available and the first component that needs to be rendered is B.

React will observe the positions of the elements. It seems that the components at position 1 and position 2 have changed and will unmount the components.

The components B and C will be unmounted and remounted on position 1 and position 2. This is ideally not required, as these components are not changing, but still, we have to unmount and remount these components, wich is a costly operation.

4.11. Import external resources async

  • JS
  • CSS
  • Fonts