background

Vue2 family barrel, Webpack, SPA, e-commerce projects

Recently doing online project first screen load time optimization * * * *, consult a large amount of data, also relatively complete sorting again before fragments of knowledge point, had planned to only in internal team for a technology to share, but then a thought, not share it out directly, maybe for which students have a little help ourselves.

More importantly, due to the limited level and energy, there will inevitably be mistakes in the article, sharing is also convenient for everyone to correct, after all, technology sharing is a process of mutual progress.

Key indicators

To do a good job, he must sharpen his tools.

Before we start, let’s take a look at the key metrics for load time (from Alibaba’s front-end monitoring tool ARMS) :

DOM Ready: domContentLoadEventEndfetchStart

Page fully loaded: loadEventStart – fetchStart

The PerformanceTiming API is not covered in detail, but can be seen in the MDN documentation

In summary, domContentLoaded and load events should be triggered as early as possible

So the question is, how do you advance this? No, let’s start with an interview question

An interview question

If you often visit the technology community, you must have seen the following interview question:

What does the browser do when you Enter the URL from the browser address bar and press Enter?

A basic pass answer would look something like this:

  1. Build the request header, and the browser checks for strong caching
  2. DNS resolution, browser check DNS cache, host match, etc
  3. Establishing a TCP Connection
  4. An HTTP request is made and the server checks the negotiation cache
  5. Data transfer, in the scenario we’re talking about, should be an HTML file
  6. According to theConnection: keep aliveField to determine whether to disconnect the TCP connection
  7. The browser parses and renders the HTML file. If the HTML file contains external links such as JS and CSS, repeat steps 3-6

We took a closer look and found that the problem was not simple, as each of the above items seemed to be related to loading speed

But the good news is, with the exception of # 7, everything else can be done ** to give the operations folks a chance to show off

OK, let’s focus on the HTML parsing and rendering process in the browser

In addition, this is actually a seemingly simple but infinitely large topic, each part of which can be expanded to say a lot

Examples include browser multi-process architecture, IPC, and scheduling of threads in the renderer process

Strong and negotiated caches, DNS resolution processes, TCP handshakes, common request headers, and even HTTP2 knowledge points

But today we’re going to focus on optimizing the first screen load, so we’re only going to focus on the rendering process and loading speed

Browser to HTML file parsing and rendering

Browsers don’t recognize text directly, so they parse the HTML in a way that produces a recognizable data structure, which is what we’ll talk about next

First, the analysis process

1. DOM construction

The DOM (Document Object Model) is an API that represents and interacts with any HTML or XML document. The DOM is a document model loaded in the browser and representing the document as a node tree, where each node represents part of the document (e.g. an element, text string, or comment).

DOM is an API for describing HTML documents, through which JS can directly manipulate document nodes.

When the browser receives a response from the server, if the response header contains content-Type: text/ HTML, it considers the response to be AN HTML file and triggers parsing

First, according to the encoding format specified in the file, such as Content-Type: Charset =UTF-8, the byte stream is translated into characters. After lexical analysis, the obtained string content can be recognized by our naked eyes, < HTML >…… ……

After syntax analysis, the string is converted into tokens, such as and , and each Token is marked with open or closed labels and text information to record the father-son sibling relationship of Nodes information to be generated later

Then, Nodes will be generated based on the Tokens and DOM will be constructed

2. CSSOM construction

The CSS Object Model is a set of APIs allowing the manipulation of CSS from JavaScript. It is much like the DOM, but for the CSS rather than the HTML. It allows users to read and modify CSS style dynamically.

Like THE DOM, CSSOM is a set of apis that provide DYNAMIC reading and manipulation styles for JS

If style resources are also introduced into the HTML, for example, by inserting a tag into the DOM, the browser will open HTTP thread downloads based on resource priority and build CSSOM immediately after the download.

The construction of CSSOM also requires lexical analysis, grammatical analysis, and ultimately Tokens

Then convert Tokens into styleRules. Each styleRule contains a selector and a set of attributes. All styleRules from the same CSS file are inserted into the same styleSheet object

Finally, the information of Nodes is also generated, and the CSSOM is finally constructed

The process of building a CSSOM Tree is very similar to that of building a DOM Tree, but the parsing algorithm involved is different.

How to build a DOM tree in a browser

How to calculate CSS in Chrome

So far, the browser’s HTML file parsing process is basically clear, as shown in the following figure:

There are many Threads running in the rendering process, among which the Main Thread is a loop in essence. It will dispatch all Worker Threads to cooperate with each other at appropriate times. DOM and CSSOM are built in separate Worker threads, so the build process is parallel without blocking each other

After the CSSOM build is complete, the browser starts rendering

Second, the rendering process

1. Build the Layout Tree

Before we start, let’s take a look at the Layout and Layout Tree explanations in the Chrome source code

The purpose of the layout tree is to do layout (aka reflow) and store its results for painting and hit-testing. Layout is the process of sizing and positioning Nodes on the page.

Layout is a process of calculating the size and position of each node on a page.

The purpose of the Layout Tree is to use the Layout and store information for subsequent drawings

In fact, building a Layout Tree iterates through the DOM Tree to exclude things that won’t be displayed, such as the display: None /contents node and the head node, Generate a Layout Object based on the style rules hit in the CSSOM Tree

Another point worth noting here is that the layout process relies on both the DOM Tree and the CSSOM Tree, so while the CSSOM build does not block the DOM build, it will end up blocking the page rendering

2, layout,

The layout process is actually a series of very complex calculations. This article will not go into details about the calculation process, but we need to know that the purpose of the calculation is mainly to confirm the size and position of each node in the Layout Tree, and finally output a box model, which will accurately capture the exact position and size of each element in the viewport. All relative measurements are converted to absolute pixels on the screen.

3, paint

Immediately after the Layout is complete, the browser issues Paint Setup and Paint events that convert the Layout Tree into pixels on the screen

The paint process is a bit too complicated for those of you who have the energy to dig into, but our topic for today is optimizing load speed, and we should really focus on parsing the process

If we could get a more streamlined DOM Tree and CSSOM Tree faster, would the subsequent rendering process be improved?

Combining theory with practice

In the previous chapter, we have basically understood the browser parsing and rendering process, but we have mastered the theory, how to apply it in our project? First, we need to take a look at the HTML package for our project

(Online code is classified, so let’s pack a Vue demo to see)

<html lang="en">
 <head>
  <meta charset="utf-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  <meta name="viewport" content="width=device-width,initial-scale=1" />
  <link rel="icon" href="/favicon.ico" />
  <title>demo</title>
  <link href="/js/about.c06f3b2f76de6a796643.js" rel="prefetch" />
  <link href="/css/app.877b7338.css" rel="preload" as="style" />
  <link href="/js/app.675cf67d3925ba86f862.js" rel="preload" as="script" />
  <link href="/css/app.877b7338.css" rel="stylesheet" />
 </head>
 <body>
  <noscript>
   <strong>We're sorry but router-demo doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
  </noscript>
  <div id="app"></div>
  <script src="/js/app.675cf67d3925ba86f862.js"></script>
 </body>
</html>
Copy the code

Parse labels from top to bottom

Parsing the head

We find meta, title, link tags in head

Meta tags are encoding, protocol, and viewport related content

The link tag does not block subsequent tag parsing, and the associated resource browser will enable network thread downloads based on priority

If you are careful, you may have noticed that in vue-CLI, there are some optimizations for file preloading: preload and prefetch, which are actually markers for resource loading priorities

  • preload: this page will be used resources, high priority loading, as far as possible to improve the page loading speed
  • prefetch: Indicates resources that may be used in future operations, which have a low priority and can be downloaded in idle time of the browser. This improves the loading speed of subsequent operations
  • Also, link is marked asrel="stylesheet"The CSS resources are loaded with the highest priority

Parsing the body

The body content is relatively simple, and the NoScript tag handles exceptions where the browser does not recognize the Scrip tag, which is largely ignored in modern browsers

An empty DIV tag with the id app is followed by a script tag

If we look carefully, we can find that the JS file has been marked preload in the link, which is a high priority resource, so the browser actually starts to download early. Therefore, when parsing the script tag, the browser will wait for the JS resource download to complete, JS thread will take over, and UI thread will be mounted. Parsing and rendering will be interrupted, and when the synchronous code in JS is finished, the UI thread will take over and continue parsing and rendering

It is important to note that the JS resource will not be executed immediately after it has been downloaded. Instead, it will be executed only after the CSS has been loaded and parsed.

Because in CSS, later rules can influence previous rules, CSSOM must be built completely to be effective

For example, if we set the font size to 14px in the CSS and then changed it to 16px, the rendering would be inaccurate, so the rendering would have to wait until the CSSOM build was complete

JS can manipulate DOM and CSSOM, so JS execution must wait for CSS to load and parse

This is a very, very important point, because there must be at least one CSS resource on our ** key rendering path **, and the loading and parsing time of this resource will eventually be fully reflected in our first screen time, for example: If we introduce other external styles in CSS with @import, we will have to wait for the new resources to load and parse before JS is executed, so it is not recommended to use @import to introduce external style resources from the perspective of the speed of the first screen

The next step is to process closed tags, HTML parsing and rendering

domContentLoaded

Let’s take a look at the definition of the domContentLoaded event in the MDN documentation

The dOMContentLoaded event is fired after the initial HTML document has been fully loaded and parsed, without waiting for the stylesheet, image, and subframe to be fully loaded.

According to the analysis in the previous chapter, we can summarize three situations:

  • No script, CSS

    In the absence of script tags, because the DOM build and CSS download are parallel, the domContentLoaded event fires after the DOM build is complete, without waiting for CSS to download and parse

  • Script, no CSS

    When the script tag is parsed, the GUI thread will be mounted, and the DOM will continue to be built after the JS synchronized code is executed

  • You have script, you have CSS

    When a script tag is parsed, the browser will wait for the CSS to download and parse, and perform the first rendering before it is executed. After the JS synchronous code is executed, the DOM will continue to be built and triggered after completion

Did you find the problem? The loading and parsing speed of CSS resources will directly affect the time node triggered by domContentLoaded event, thus affecting the loading speed of the first screen. Therefore, this should be the point we should focus on optimization, to accelerate the loading and parsing of CSS resources

Also, JS loading and execution can block DOM builds, so another optimization we need to focus on is speeding up the loading and execution of JS resources

load

Let’s also look at the definition of a load event in the MDN documentation

The Load event is triggered when the entire page and all dependent resources such as stylesheets and images have finished loading.

It differs from domContentLoaded, which fires as soon as the PAGE DOM is loaded, without waiting for dependent resources to load.

In other words, the browser does not trigger the load event until the DOM is built and the styles, images, and asynchronously loaded resources triggered in the synchronous code are fully loaded, so another key optimization is to speed up the loading of resources mounted in the first-screen DOM

The load event is triggered, indicating that the page is fully loaded, and the fetch logic in the performance metric ends here

To sum up, there are only three optimizations

  • Accelerates the loading and parsing of CSS resources
  • Accelerate the loading and execution of JS resources
  • Accelerates the loading of resources mounted in the DOM on the front screen

Now, let’s look at how do we optimize

Key render path

In fact, we summarized the three key optimization points, are ** key rendering path **

The key render path is all the paths that the browser takes during the full first screen load, that is, every step of the HTML parsing starts until the load event is triggered

Accelerate the loading and parsing of CSS and JS resources

1. Higher loading priority

We can give a high load priority to critical resources, such as preload, so that the browser loads them ahead of time rather than waiting for them to be used

Note, however, that too much preload can affect the loading of critical resources, because the number of TCP connections within a browser’s domain name is limited, such as chrome’s limit of 6

Therefore, non-critical render path resources cannot be preloaded

2. Smaller resource volumes

  • Open Gzip.

    Gzip can greatly reduce the volume of resources and is a simple and effective way to improve performance. Data in the production environment shows that the main resource file of 730K can be compressed to about 210K, which is a significant effect.

  • Code Splitting

    In the Demo above, we see such a resources: < link href = “/ js/about. C06f3b2f76de6a796643. Js” rel = “prefetch” / >

    During Webpack packaging, asynchronously imported resources are bundled into a separate chunk file that is loaded only when the file needs to be executed. This is the premise for the feasibility of code splitting

    1. When we first enter a page (such as Home), we don’t need to load resources from other pages (such as About), so we can split up resources for each page and load them when we really need them. So we can do a route lazy load, as shown in Demo:

      // ...
      const routes = [
        {
          path: '/'.name: 'Home'.component: Home
        },
        {
          path: '/about'.name: 'About'.// route level code-splitting
          // this generates a separate chunk (about.[hash].js) for this route
          // which is lazy-loaded when the route is visited.
          component: () = > import(/* webpackChunkName: "about" */ '.. /views/About.vue')}]const router = new VueRouter({
        mode: 'history'.base: process.env.BASE_URL,
        routes
      })
      export default router
      
      Copy the code

      The About component is loaded asynchronously with the import() syntax. The About component is packaged into a separate chunk, so if we first access the Home page, we don’t have to load the About resource synchronously

    2. If we have a popup component HomeModal in the Home page, it will be displayed only when the user triggers the query event. In fact, we do not need to load HomeModal resources synchronously in the first screen, such as:

           <template>
             <div class="home">
               <div @click="showModal = ! showModal">Display the pop-up</div>
               <HomeModal v-if="loadModal" v-show="showModal">
             </div>
           </template>
           
           <script>
           const HomeModal = () = > import('@/components/HomeModal.vue')
           export default {
             name: 'Home'.components: {
               HomeModal
             },
             data() {
               return {
                 showModal: false.loadModal: false,}},methods: {
               // Load time is specified based on the service
               lazyLoadHomeModal () {
                 this.loadModal = true}}}</script>
      Copy the code

      With v-if set to true when appropriate, the browser will asynchronously load and render the HomeModal component when Vue mounts it, and the subsequent display and hiding can still be controlled by v-show

      Also, non-UI resources can be loaded asynchronously, such as:

      const getCat = () = > import('./cat.js')
      // Load time is specified based on the service
      getCat()
        .then({ meow } => meow())
      Copy the code

      Code Splitting is a very effective way to reduce the size of key resources, but Code Splitting should be based on your own business and should not be overdone. Ensure that asynchronously loaded resources are not critical and have a good loading timing trigger strategy.

    3. The UI library is loaded on demand

      Your project may use some third-party UI libraries, which can greatly improve development efficiency, but you may only be using some of the UI components in the library, so loading on demand is essential. The load on Demand configuration is explained in detail in the READme of each UI library and can be found in the official documentation

    4. Uglify

      Good indentation and typography can certainly improve the readability of our code, but packaging code into production environment can remove unnecessary whitespace and reduce the number of characters, which is usually taken into account by packaging tools.

      But there are many other things you can do, such as checking DeadCode in your code, removing logs in production, removing unwanted styles, and so on. Because useless resources take up not only our resource loading time, but also our resource parsing time, this step kills two birds with one stone.

      Sometimes we use Base64 to load image resources in CSS, which has some advantages, but is strongly discouraged.

      As we mentioned above, CSS will block rendering, and Base64 encoding will make the file larger, which will eventually be reflected in the packaged CSS file, unnecessarily extending the first screen time, so it is more recommended that CDN introduce image resources.

  • The CDN to accelerate

    There is no need to say more about the benefits of CDN, which are also key optimization points

  • HTTP2

    Fragmentation resources that go through Code Splitting have better load performance thanks to HTTP2’s multiplexing features.

    For details about HTTP2, see this article HTTP2

3. Faster execution

There’s not much we can control about the parsing speed of CSS, but a better, more refined style structure is definitely a benefit

The focus of our analysis is the execution speed of JS. Since our project is based on Vue as a Demo, we will analyze the whole life cycle of Vue in detail

We know that in the Vue project, the Webpack entry is main.js, so let’s look at main.js

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'

Vue.config.productionTip = false

new Vue({
  router,
  store,
  render: h= > h(App)
}).$mount('#app')
Copy the code

The Demo’s main.js is still pretty simple, but in general, you might want to include some global logic, such as global components, global Filters registration, and some business logic that might need global processing. Check to see if there is a better way to handle non-essential logic

Then we need to see, what does new Vue() and mount() do

It’s not practical to read the entire flow of Vue in one article, so I’ve summarized the parts of New Vue() and Mount () where performance tuning can be tweaked

  1. initState()

    Data hijacking and responsivity for data and props in Vue is done in this method. So we should check to see if all the data we use in our business should be responsive. If it is some data irrelevant to the UI, don’t put it in data

  2. mount()

    In Vue, the essence of mount() is to compile the template file to generate the Render function, execute the Render function to generate the virtual DOM, and finally generate the real DOM from the virtual DOM and mount it to the target node, which in our Demo is Render: H => H (App), so we also have to look at the App component

  3. APP

    <template>
      <div id="app">
        <div id="nav">
          <router-link to="/">Home</router-link> |
          <router-link to="/about">About</router-link>
        </div>
        <router-view/>
      </div>
    </template>
    Copy the code

    The template of the APP is very simple, which is to match the target component for us based on the Router

    But in our real business, APP components are usually not that simple. If you have other components introduced, please refer to our previous section for non-critical resource asynchronous loading

  4. Router

    The Rrouter basically matches the target component for us, and if the component is loaded asynchronously, it will request the resource. However, we know that vue-Router actually exposes some hook functions for us, and this part of logic will be carried out before the target page is actually mounted. Therefore, if we have time-consuming or even blocking operations in the target component or even globally mixed in the front hook function, please think about a better way to handle business.

  5. The target component

    The target component is the single page we want to access. Instead, each component executes vue.extend (), resulting in a single instance of Vue that incorporates global attributes. The target component also goes through the full Vue lifecycle, only this time, the Vue compiles the complex template file on the target page and mounts the DOM to the target node. We know that DOM manipulation is very expensive, so we need to consider whether the first rendering of the target page is necessary to generate the full DOM structure. Of course not, there’s actually a great solution in the industry: skeleton screens.

Here is a strong anli about this document, Vue technology revealed, the author is very deep and methodical for us to analyze the Vue2 source code of the execution logic, I have a new harvest every time I see, I think every Vue developers need to read the Vue2 source code, even though Vue3.0 has been released, but the idea is still the same.

Accelerates the loading of resources mounted in the DOM on the front screen

In the business scenario we are talking about, the LOAD is triggered when the DOM mounted resource finishes loading, meaning that the page is not fully loaded until this point

In the previous chapter, we talked about the use of ** skeleton screen ** on the target page. A simple and beautiful skeleton screen display can provide a good user experience. More importantly, the DOM structure of the skeleton screen can be very simple, and no additional resources need to be mounted. It helps a lot with our domContentLoaded and load events.

If you don’t want to use skeleton screens, then consider how to simplify the loading speed of mounted resources in the first-screen render DOM, such as thumbnail usage, image compression, WebP, CDN, etc

Behind the skeleton plate

We did our homework and finally greatly optimized the speed of the first screen. At least the number of ARMS fetching is much more beautiful. The leader is very happy.

However, the rendering page after the skeleton screen will not be included in the time of the first screen, but it will still affect the user experience. Therefore, we should keep on optimizing the DOM structure related to our real business.

The last

This article is more of a record of the performance optimization work, so there are more summative things, and there is no simple, the reference or reference of the data is as follows

Vue technology revealed

Key render path

From browser multi process to JS single thread, JS running mechanism is the most comprehensive combing

(1.6W word) browser soul ask, how many can you catch?

HTTP2,

How to build a DOM tree in a browser

How to calculate CSS in Chrome

MDN document