Server-side rendering

In traditional Web development, web content is rendered on the server and then transferred to the browser. With its excellent user experience, single-page application has gradually become the mainstream. In single-page application (SPA), the specific content of the page is rendered by JS, which is generally rendered by the client. SPA has two significant drawbacks: SEO is not friendly (the content rendered on the page cannot be found in the HTML source file); The first screen is too slow to load (rendering must wait until the necessary JS files have been loaded and executed).

The SSR solution renders the full front-screen DOM structure from the back end, and the front end only needs to activate the application to continue to run in SPA mode.

To see an example from the Vue SSR guide, focus on the renderToString method, which is responsible for generating static HTML strings:

const Vue = require('vue')
// Create an Express application
const server = require('express') ()// Extract the renderer instance
const renderer = require('vue-server-renderer').createRenderer()

server.get(The '*', (req, res) => {
  // Write Vue instances (virtual DOM nodes)
  const app = new Vue({
    data: {
      url: req.url
    },
    // Write the contents of the template HTML
    template: '
      
accesses the URL: {{URL}}
'
}) RenderToString is a key method of converting Vue instances into real DOM renderer.renderToString(app, (err, html) => { if (err) { res.status(500).end('Internal Server Error') return } // Insert the rendered real DOM string into the HTML template res.end(` <! DOCTYPE html> <html lang="en"> <head><title>Hello</title></head> <body>${html}</body> </html> `) }) }) server.listen(8080) Copy the code

It’s important to keep this apart because SSR doesn’t simply concatenate strings. More specifically, it runs the Vue/React code through nodes to convert the virtual DOM into the real DOM. It is then activated on the client side and continues to run as a single-page application, which traditional JSP development cannot do.

Although app-side rendering looks great, why are so many sites not using SSR? Why do we still need prerender-spa-plugin?

The nature of server rendering is to offload what the browser should do to the server. Server performance is much better than browser performance, but consider that almost every user has a browser and the number of servers is limited, and when the rendering pressure is concentrated, the server is bound to have problems.

Therefore, using SSR to optimize performance or SEO needs to be carefully considered, first try all the available methods (pre-rendering), still can not meet the performance requirements, and then consider server rendering.

Browser Rendering optimization

To do front-end performance optimization at the browser level, you must first understand the basic workings of the browser.

Start with the most important part: the browser kernel. It is divided into two parts: rendering engine and JS engine. The rendering engine includes many parts: HTML interpreter, CSS interpreter, layout, network, storage, graphics, image decoder, and so on. Common browser kernels can be divided into: Trident(IE), Gecko(Firefox), Blink(Chrome, Opera), Webkit(Safari). We take Webkit as an example to do a specific analysis of the browser rendering process.

First of all, how do browsers convert HTML/CSS/JS resources into visible images?

This is the result of various modules within the browser kernel working together:

  • HTML interpreter: Output HTML into a DOM tree through lexical analysis
  • CSS interpreter: Parses CSS documents and generates style rules
  • Layer layout calculation module: Layout calculates the exact position and size of each object
  • View drawing module: draw images of specific nodes and render pixels to the screen
  • JS engine: Compiles and executes JS code

Now you can comb through the render process:

  1. First, the HTML interpreter parses the HTML, generates a DOM tree, and in the process, makes requests for various external resources needed for page rendering
  2. The browser parses and loads the CSS style information to generate the CSSOM tree, which is merged with the DOM tree to generate the Render tree
  3. Calculate the layer Layout, recursively call from the root node, calculate the size and position of each element, give the specific coordinates on the screen that each node should appear, and finally get the Layout of the Render tree based on the render tree.
  4. Draw layers, convert each layer to a pixel, and decode the media file
  5. Integrate layers, output data from CPU to GPU to draw on the screen

To sum it up in one sentence, the HTML-based DOM tree is combined with the CSSOM based tree to generate a layout rendering tree that the browser draws images from and displays on the screen.

Also note that parsing of the DOM tree is parallel to parsing of the CSSOM tree.

CSS optimization during rendering

For each new element added to the DOM tree, the CSS style sheet is iterated through the CSS engine and the style rule matching the element is applied to the element. This is where you can start to optimize your CSS by writing sensible CSS rules.

There is a rule that many people don’t think of when the CSS engine looks up stylesheets: CSS selectors are matched from right to left.

#app div {
	color: red
}		
Copy the code

Conventional wisdom suggests that the browser should first find the element with the ID app, and then look for the element whose descendants are divs.

But goose, that’s not what happened.

The CSS engine looks for any element with a tag of div and then verifies that its parent element is #app. Obviously, the query overhead can be considerable.

Therefore, do not use the wildcard * when clearing the browser default style. This will cause the browser to iterate over every element, which may cause considerable performance loss in a large application.

With this knowledge in mind, you can come up with the following common performance improvement solutions:

  • Avoid wildcards and do a specific default style sweep for the desired elements
  • Focus on inheritance of CSS properties to avoid redefining certain styles
  • Use tag selectors less and think about how many there are on a pagedivandspan, use class selectors instead
  • Reduce the number of tag nesting layers and the descendant selector overhead is high

Optimization for loading order of CSS and JS files

  • The CSS block

    As mentioned earlier, CSSOM and DOM trees work together to generate render trees, so obviously you can’t do without both. So, CSS blocks DOM rendering (but not DOM parsing) by default. Only after the CSS has been parsed and the CSSOM tree has been generated can the rendering process proceed (otherwise the user will see an ugly page with no style). When parsing to HTML, the construction of the CSSOM tree starts only when the link tag or style tag is parsed. In most cases, DOM trees are built as quickly as CSSOM trees.

    Therefore, it is necessary to download CSS resources to the client as soon as possible (using CDN to optimize the loading speed of static resources) and as soon as possible (in the head tag) to shorten the first rendering time.

  • JS blocked

    JS blocking is different from CSS blocking. As mentioned earlier, the JS engine exists independently of the rendering engine. When a script tag is parsed, the browser gives control to the JS engine, and DOM parsing and rendering are blocked. Until the script is finished, control is returned to the rendering engine and the building continues. The reason for this is also that it is not clear whether the DOM will be manipulated in the JS file, resulting in messy rendering.

    Consider a scenario where you have a very large JS file that doesn’t rely on DOM elements internally. Do you still need to block rendering?

    Obviously not.

    For similar scenarios, JS provides three loading methods:

    • Normal mode: blocks browser rendering and loading

      <script src="index.js"></script>
      Copy the code
    • Defer mode: JS loads asynchronously, execution is deferred until DOM parsing is complete and the DOMContentLoaded event is about to be triggered, and the JS files marked defer start executing one by one

      <script defer src="index.js"></script>
      Copy the code
    • Async mode: Load is asynchronous and is executed immediately after loading

      <script async src="index.js"></script>
      Copy the code

    In general, async can be used when the script is not strongly dependent on other scripts and the DOM. When you rely on script execution order and other script execution results, defer is an option.

DOM optimization

Most front-end developers know that DOM manipulation is expensive. At present, excellent front-end frameworks also deal with this pain point, such as the virtual DOM in vue. js, where the Patch algorithm reduces DOM operations as much as possible through JS operations.

Before looking at how to optimize, why is DOM manipulation so slow?

Think of DOM and JavaScript as islands, each connected by a toll bridge. — High Performance JavaScript

When we manipulate the DOM in JS, we are actually communicating with the JS engine and the rendering engine, and as mentioned, the two engines are implemented independently. For this reason, communication between the two engines is very performance intensive, and the number of communications naturally leads to performance problems. So reducing DOM manipulation is not a blind alley.

Reduce backflow and redraw

When we manipulate the DOM and cause a change in its style, the render tree changes, which ultimately trigger backflow and redraw of the browser.

  • Backflow: Changes to the DOM result in geometry changes that the rendering engine needs to recalculate the layout (the dimensions or positions of other locations are affected) and then render the results.
  • Redraw: Changes to the DOM do not affect the geometry, but only the display effect. In this case, you can directly draw a new style without recalculating the layout, resulting in less performance impact than redraw. It can also be said that backflow always leads to redrawing, but redrawing does not necessarily lead to backflow.

Common operations that trigger backflow include changing the geometry of DOM elements, changing the structure of the DOM tree, and getting DOM attribute values (offsetHeight, scrollTop, and so on).

Although it is often impossible to avoid operations that cause backflow or redraw, there are some common optimizations for certain scenarios:

  • Cache fuses to avoid frequent triggers

    const el = document.getElementById('app')
    for (let i = 0; i < 10; i++) {
    	el.style.top = el.offsetTop  + 10 + "px";
    }
    // After optimization, the calculated value is applied to the DOM
    const el = document.getElementById('app')
    let offTop = el.offsetTop
    for (let i = 0; i < 10; i++) {
    	offTop +=  10;
    }
    el.style.top = offTop + 'px'
    Copy the code
  • Instead of changing styles line by line, use class names to merge style changes

    el.style.color = "red"
    el.style.border = "1px solid"
    el.style.padding = "10px"
    
    // After optimization, modify the class name directly to define the style in CSS
    el.classList.add('special')
    Copy the code
  • Take the DOM offline, modify it, and bring it online again

    const container = document.getElementById('container')
    container.style.width = '100px'
    container.style.height = '200px'
    container.style.border = '10px solid red'
    container.style.color = 'red'
    
    // After the optimization, the changes after offline will not be backflow or redraw
    let container = document.getElementById('container')
    container.style.display = 'none'
    container.style.width = '100px'
    container.style.height = '200px'
    container.style.border = '10px solid red'
    container.style.color = 'red'
    container.style.display = 'block'
    
    Copy the code

Reduce unnecessary DOM manipulation

// Get container only once
let container = document.getElementById('container')
for(let count=0; count<10000; count++){ container.innerHTML +=' I am a small test '
} 
Copy the code

In the example above, we modified the DOM tree 10,000 times out of 10,000 loops. In this case, you should use JS computation instead of DOM manipulation:

let container = document.getElementById('container')
let content = ' '
for(let count=0; count<10000; count++){// Start with the content
  content += ' I am a small test '
} 
// When the content is processed, DOM changes are triggered
container.innerHTML = content
Copy the code

Asynchronous update strategy

When we use Vue or React interfaces to modify data, views are not updated immediately. Instead, they are pushed into a queue and then updated in batches. This is called an asynchronous update strategy and avoids overrendering.

<div>{{content}}</div> this.content = 'aaa' this.content = 'BBB' this.content = 'CCC' The render function of the component is executed only onceCopy the code

For more details on how to implement asynchronous updates in Vue, please refer to this article.