Featured image of post What optimizations can be utilized for frontend performance?

What optimizations can be utilized for frontend performance?

What performance optimizations can be done on the frontend?

In an interview, I was asked about frontend performance optimizations. After a series of answers, the interviewer further asked: What performance optimizations can be done on the frontend?

Frontend optimizations can be approached in several directions:

  • Network Optimization

  • Page Rendering Optimization

  • JS Optimization

  • Image Optimization

  • webpack Packaging Optimization

  • React Optimization

  • Vue Optimization


Network Optimization

DNS Pre-fetching

Use the rel attribute of the link tag to set dns-prefetch and get the IP address corresponding to the domain name in advance.

Use Cache

educe server pressure and quickly get data (see here for strong cache and negotiated cache).

Use the CDN (Content Distribution Network)

The physical distance between the user and the server also affects the response time.

Compress Responses

Reduce the size of the response package generated by HTTP requests to improve performance by decreasing transmission time.

Use Multiple Domains

Modern browsers such as Chrome will have the same domain name limit and concurrent download number. Using different domain names can maximize download threads, but try to keep within 2~4 domain names to avoid DNS query loss.

Avoid Null Image src

Empty string for src attribute still makes the browser issue an HTTP request to the server.

IE sends a request to the directory where the page is located; Safari, Chrome, and Firefox send a request to the page itself; Opera does not perform any operation.

Page Rendering Optimization

Process of Webkit rendering engine

  • Handles HTML and builds a DOM tree

  • Handles CSS and builds a CSS rules tree (CSSOM)

  • Merges the DOM Tree and CSSOM Tree to form a Render Tree.

  • Layouts according to the Render Tree and calculates the position of each node

  • Calls GPU drawing, composites layers, and displays on the screen

Avoid CSS blocking

CSS affects the construction of the Render Tree and may block page rendering. Therefore, you should try to load the CSS resource as early (put the CSS in the head tag) and as fast (use CDN to optimize the loading speed of static resources) as possible.

Reduce the complexity of CSS selectors

Browsers read selectors from right to left.

  • Reduce nesting: Don’t exceed three levels, and be cautious about using descendant selectors, which have high costs.

  • Avoid using universal selectors, match only the elements needed

  • Take advantage of inheritance, avoid repetitive matches and definitions

  • Use class selectors and id selectors correctly

Avoid using CSS expressions

CSS expressions are calculated frequently.

Avoid JS blocking

JS can modify CSSOM and DOM, so JS can block the parsing and rendering of pages, and it will wait for the loading of CSS resources. In other words, JS will take over control of the rendering engine. Therefore, we need to add defer or async to our JS resources to delay the execution of JS scripts.

Use externally linked JS and CSS

In real environments, using external files generally leads to faster pages because JavaScript and CSS have a chance to be cached by the browser. For inline situations, HTML documents usually aren’t configured to be cacheable, so for every HTML document request, JavaScript and CSS need to be downloaded. Therefore, if JavaScript and CSS are in external files, the browser can cache them, the size of HTML documents will be reduced without increasing the number of HTTP requests.

Use font icons (Iconfont) instead of image icons

  • Images increase the number of network requests, thereby slowing down page load time.

  • Iconfont can scale well and won’t add extra requests.

First Screen Load Optimization

  • Use a skeleton screen or animation to optimize user experience

  • Load resources on demand; delay loading of resources not needed on the homepage

Reduce Repaint and Reflow

  • Add multiple nodes using documentFragment: This is not part of the actual DOM, and will not cause repaint and reflow

  • Use translate instead of top because top will trigger reflow, but translate will not. So translate saves one layout time compared to top

  • Replace display: none with visibility, because the former will only cause repaint, while the latter will trigger reflow (changing the layout); replace visibility with opacity, visibility will trigger repaint (paint), but opacity won’t.

  • Modify the DOM offline, for example: First set the DOM to display:none (causing one reflow), then you modify it 100 times, and then display it again

  • Don’t put the property values of DOM nodes in a loop as variables of the loop.

For example in Javascript:

1
2
3
4
for (let i = 0; i < 1000; i++) {
  // Getting offsetTop will cause a reflow because you need to get the correct value
  console.log(document.querySelector('.test').style.offsetTop)
}
  • Try to use table layout less, because every time a cell’s layout changes, the entire table will go through reflow and repaint.

  • Don’t frequently manipulate DOM nodes. It’s better to write the styles that need to be manipulated into classes in advance, and then modify them when needed. A single reflow and repaint is much less costly than multiple reflows and repaints.

  • Choose the speed of animation implementation. The faster the animation speed, the more reflows, you can also choose to use requestAnimationFrame.

  • Every time you access the offset attributes of the DOM, such as getting an element’s scrollTop, scrollLeft, scrollWidth, offsetTop, offsetLeft, offsetWidth, offsetHeight, etc., the browser will reflow to get the latest value in order to ensure the correctness of the value. Therefore, if you need to operate multiple times, take a cache after the extraction. And don’t access DOM offset attributes in a for loop. When used, it is best to define a variable and assign the needed value to it, cache the value, and reduce the number of reflows and repaints.

  • Convert frequently running animations to layers. Layers can prevent this node’s reflow from affecting other elements. For example, for the video tag, the browser will automatically turn this node into a layer.

Performance Optimization in JS

Use event delegation

Implement debounce and throttle

Try not to use JS animations.

CSS3 animations and canvas animations perform better than JS animations.

Multithreading

Start a webWorker for complex calculations to avoid page freezing.

Cache Calculation Results

Reduce the number of calculations, such as using computed in Vue.

Image Optimization

Using Sprites

Optimize by reducing the number of HTTP requests.

Lazy Loading Images

Load the image as it is about to enter the viewport (For how to judge whether the image enters the viewport, please refer to here)

Use CSS3 Instead of Images

There are many images that can be drawn with CSS effects (gradients, shadows, etc.). In this case, choosing CSS3 effects is better.

Image Compression

There are two ways to compress images. One is to compress them through an online website. The other is to use the webpack plugin image-webpack-loader, which is based on the Node library imagemin for image compression.

Use Progressive JPEG

Using progressive JPEG will improve the user experience.

Use WebP Format Images

WebP is a new image file format that provides both lossy and lossless compression. With the same image quality, the volume of WebP is smaller than PNG and JPG.

Webpack Packaging Optimization

Narrow Down the Range of Loader Matches

  • Optimize loader configuration

  • Use the three configuration items test, include, exclude to narrow down the processing range of the loader.

  • It’s recommended to use include.

1
include: path.resolve(__dirname, "./src"),

Resolve.Modules

resolve.modules is used to configure which directories webpack goes to look for third-party modules, by default it’s node_modules.

By default, it looks for third-party modules in the node_modules directory under the current project directory. If it doesn’t find them, it will go to the ../node_modules directory above. If it doesn’t find them there, it will go to the ../../node_modules directory, and so on. This is similar to Node.js module search mechanism.

If we install all third-party modules in the root directory of the project, we can specify this path directly.

1
2
3
4
5
module.exports={
 resolve:{
 modules: [path.resolve(__dirname, "./node_modules")]
 }
}

Resolve.Extensions

If the import statement does not have a file suffix, webpack will automatically add the suffix and then try to find out whether the file exists or not.

  • Keep the list of suffixes as small as possible

  • Try to put a suffix in the import statement as much as possible.

If you want to optimize to the extreme, it’s not recommended to use extensionx, because it will consume some performance, even though it can bring some convenience.

Extract CSS

With the help of mini-css-extract-plugin: this plugin extracts CSS to a separate file, creates a CSS file for each JS file containing CSS, and supports on-demand loading of CSS and SourceMaps.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
 {
 test: /\.less$/,
 use: [
  // "style-loader", // No need for style-loader anymore, use MiniCssExtractPlugin.loader instead
  MiniCssExtractPlugin.loader,
  "css-loader", // Compile css
  "postcss-loader",
  "less-loader" // Compile less
 ]
 },
plugins: [
  new MiniCssExtractPlugin({
   filename: "css/[name]_[contenthash:6].css",
   chunkFilename: "[id].css"
  })
 ]

Code Compression

  • JS Code Compression

With mode:production, terser-webpack-plugin is used.

1
2
3
4
5
6
7
8
9
module.exports = {
    // ...
    optimization: {
        minimize: true,
        minimizer: [
            new TerserPlugin({}),
        ]
    }
}
  • CSS Code Compression

Using css-minimizer-webpack-plugin

1
2
3
4
5
6
7
8
9
module.exports = {
    // ...
    optimization: {
        minimize: true,
        minimizer: [
            new CssMinimizerPlugin({})
        ]
    }
}
  • HTML File Code Compression
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
module.exports = {
    ...
    plugin:[
        new HtmlwebpackPlugin({
            ...
            minify:{
                minifyCSS:false, // Whether to compress css
                collapseWhitespace:false, // Whether to collapse whitespace
                removeComments:true // Whether to remove comments
            }
        })
    ]
}

When you set minify, it will actually use another plugin html-minifier-terser.

  • File Size Compression

Compress the size of the file to reduce the bandwidth loss during HTTP transmission.

1
2
3
4
5
6
7
npm install compression-webpack-plugin -D
new ComepressionPlugin({
    test:/.(css|js)$/,  // files that need to be compressed
    threshold:500, // Set filesize for compression to start
    minRatio:0.7, // minimum compression ratio
    algorithm:"gzip", // compression algorithm to be used
})
  • Image Compression

Normally, after packaging, the size of some image files is far larger than that of js or css files, so image compression is quite important.

The configuration method is as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
module: {
  rules: [
    {
      test: /.(png|jpg|gif)$/,
      use: [
        {
          loader: 'file-loader',
          options: {
            name: '[name]_[hash].[ext]',
            outputPath: 'images/',
          }
        },
        {
          loader: 'image-webpack-loader',
          options: {
            // Configuration for compressing jpeg
            mozjpeg: {
              progressive: true,
              quality: 65
            },
            // Using imagemin**-optipng to compress png, with enabled: false to turn it off
            optipng: {
              enabled: false,
            },
            // Using imagemin-pngquant to compress png
            pngquant: {
              quality: '65-90',
              speed: 4
            },
            // Configuration for compressing gif
            gifsicle: {
              interlaced: false,
            },
            // Enable webp, will compress jpg and png images into webp format
            webp: {
              quality: 75
            }
          }
        }
      ]
    },
  ]
}

Tree Shaking to Remove Dead Code

Tree Shaking is a term in computing that means eliminating dead code, or code that is no longer being used. It relies on the static syntax analysis of ES Module (without executing any code, we can clearly understand the dependency relationship of the module)

Webpack provides two different approaches to implementing Tree Shaking:

  • usedExports: By marking whether certain functions are used and then optimizing them through Terser

  • sideEffects: It skips the entire module/file and directly checks if the file has side effects

Different configuration plans have different effects.

usedExports

The configuration method is very simple, you just need to set usedExports to true.

1
2
3
4
5
6
module.exports = {
    ...
    optimization:{
        usedExports
    }
}

After using, the unused code in webpack packaging will have an “unused harmony export mul” comment added to notify Terser that this section of code can be deleted when optimizing.

sideEffects

sideEffects is used to inform the webpack compiler which modules have side effects. The configuration method is to set the sideEffects attribute in the package.json.

If sideEffects is set to false, it informs webpack that it can safely delete unused exports.

If some files need to be retained, it can be set in an array form.

1
2
3
4
"sideEffecis":[
    "./src/util/format.js",
    "*.css" // All CSS files
]

The above are all about the tree shaking of javascript. CSS can also achieve tree shaking.

css tree shaking

You can install the PurgeCss plugin for CSS optimization.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
npm install purgecss-plugin-webpack -D
const PurgeCssPlugin = require('purgecss-webpack-plugin')
module.exports = {
    ...
    plugins:[
        new PurgeCssPlugin({
            path:glob.sync(`${path.resolve('./src')}/**/*`), {nodir:true}// all files in src
            satelist:function(){
                return {
                    standard:["html"]
                }
            }
        })
    ]
}
  • paths: Represents which directories need to be analyzed. It is used in conjunction with glob.

  • By default, Purgecss will remove the style of our HTML tags. If we want to keep it, we can add a safelist attribute.

Code Splitting

This is the process of dividing code into different bundles, which can then be loaded on demand or in parallel.

By default, all the JavaScript code (business code, third-party dependencies, currently unused modules) is loaded on the homepage, which may impact the page load speed.

Code splitting can break the code into smaller bundles and control the load priority of resources to improve the performance of code loading.

This can be achieved by using the SplitChunksPlugin, which is already integrated in webpack by default, and only needs configuration.

1
2
3
4
5
6
7
8
module.exports = {
    ...
    optimization:{
        splitChunks:{
            chunks:"all"
        }
    }
}

The main attributes of splitChunks are as follows:

  • Chunks: This handles synchronous or asynchronous code.

  • minSize: The size of the split package. It must be at least minSize. If the package size does not exceed minSize, this package will not be split.

  • maxSize: Packages larger than maxSize will be split into packages that are not smaller than minSize.

  • minChunks: The number of times it is quoted, the default is 1.

Multi-threaded Packaging to Improve Packaging Speed.

Vue

  • Add a key in v-for

  • Lazy load routes

  • Use third-party plugins on demand

  • Make reasonable use of computed and watch

  • Avoid using v-if while using v-for

  • Destroy events when destroy, such as events added by addEventListener, setTimeout, setInterval, etc.

  • When rendering large lists, optimize by using virtual lists.

  • Use Keep-Alive to cache components and avoid repeated rendering of components.

  • Use Teleport to render the component’s DOM into a designated container, thus avoiding unnecessary DOM operations.

  • Use Suspense to display a placeholder while the component is not yet loaded, thus avoiding repeated rendering of components.

  • Use Fragments to avoid unnecessary DOM operations.

  • Use Memoization to avoid unnecessary calculations.

React

  • Add a key when looping with map

  • Lazy load routes

  • Use third-party plugins on demand

  • Use scu, memo or PureComponent to avoid unnecessary rendering

  • Make reasonable use of useMemo, memo, useCallback

These three are all applied in caching results to avoid unnecessary calculations or rendering when the dependent value has not changed.

  • useCallback is used to “remember” a function. When its dependent item does not change, the reference of the function will not be reassigned with the refresh of the component. We can use useCallback to modify it when we think a function does not need to update its reference address with the update of the component.

  • React.memo is used to “remember” a component. When it receives props that have not changed, it will return the result of the last rendering, and will not re-execute the function to return a new rendering result.

  • React.useMemo is a kind of “memory” for value calculation. When the dependent item does not change, there is no need to calculate it again, just use the previous value. For the component, one of the benefits is that it can reduce some calculations and avoid some unnecessary rendering. When we encounter some data that needs to be calculated within the component, we can consider React.useMemo.