preface

Have you experienced the following scenarios:

  • In the interview
    • Talk about your understanding of performance optimization
    • What happens to the entire page when you enter the URL?
    • .
  • In the work
    • Page loading is slow, I don’t know is the front end or back end problem
    • Page interaction is slow, I don’t know exactly where the problem
    • .

If you encounter this type of problem, I hope my recent knowledge of performance optimization can help you. This article will first analyze the rendering process and then share the optimization solution.

First, the entire process after the browser receives the URL is divided into the following stages:

  1. The network request thread is enabled
  2. Establishing HTTP requests (DNS resolution, TCP connection)
  3. Front and back end interaction (back end processing flow, browser cache)
  4. Key render path (HTML, CSS, JS organization render together)

Rendering process Analysis

The network request thread is enabled

The browser receives the URL we entered into the web request thread. This is done inside the browser. First the browser parses the URL

The title The name of the note
Protocol Head agreement How can the browser handle the file to be opened

Common ones include HTTP, FTP, and Telnet
Host Host domain name /IP address The host domain name or IP address is resolved by DNS
Port The port number The identity used for the connection between the requester and the corresponding program
Path Directory path The requested directory or file name
Query Query parameters The parameters passed by the request
Fragment fragment Usually as a front-end route or anchor point

URL structure Protocol: / / Host: Port/Path? Query# fragments, example: http://example.com/users/1?foo=bar#abc

After parsing the URL, if it is HTTP protocol, the browser will create a new network request thread to download the required resources, for the learning of threads and processes, you can refer to teacher Ruan Yifeng’s blog post thread and process a simple explanation

The DNS

DNS resolution mainly converts the HOST field in the URL into the specific IP address in the network by querying. The domain name we often see is only for the convenience of memory, and the IP address is the “house number” of the server to be accessed on the network.

DNS resolution process:

The flowchart is as follows:

If the IP address is not found in the browser cache, the system will search the DNS cache of the system. If the IP address is not found in the browser cache, the system will try to find the IP address from the hosts file in the system.

If nothing is found on the local host, it is then queried on the local DNS server. If the IP address is not found, the local DNS server iteratively queries the root DNS server, COM TOP-LEVEL DNS server, and permission DNS server, and finally returns the IP address of the target server to the host. If the IP address is not found, an error message is returned.

Therefore, DNS resolution is a time-consuming process. If too many domain names need to be resolved, the first screen rendering time will be affected.

A TCP connection

After DNS resolution, the TCP connection is started. Since TCP is a connection-oriented communication protocol, the connection between the client and the server needs to be established before the data transfer, which is commonly referred to as the “three-way handshake”.

The “Three-way Handshake” diagram:

“Three handshakes” detailed analysis:

  1. First handshake: The client generates a random number seq, assuming t, and sets the SYN flag bit to 1. This data is sent to the server, and the client enters the wait state.
  2. Second handshake: When the server receives SYN=1 from the client, it knows that the client is requesting a connection, sets BOTH SYN and ACK to 1, assigns the random value t+1 of seQ sent by the client to ACK, and then generates a random number seq=k, packages the data and sends it to the client. As an acknowledgement of a client request to connect.
  3. The third handshake: The client receives the data sent by the server, check to see if the ack for t + 1, whether an ack is equal to 1, if all right, just send the server side of seq random number k + 1 assignment to ack, the data sent to the server to verify that the server response, the server side according to whether the ack is equal to k + 1 to decide whether to establish a connection.

When the user closes the tag or the request completes, the TCP connection performs a “four wave”

“Four Waves” analysis chart:

Detailed analysis of “Four Waves” :

  1. First wave: The client sends the command FIN=M to the server, and then enters the wait state FIN_WAIT_1, indicating that the client has not requested data from the server. However, if the server still has unfinished data, the client can continue to send data.
  2. Second wave: After receiving a FIN packet from the client, the server sends an ACK =M+1 (ACK =M+1) to notify the client that it has received the close request. However, the server may still have data to send. Therefore, the client should wait.
  3. Third wave: After sending all data, the server sends a FIN=N packet to the client to inform it that it is ready to close the connection, and waits for the client to finally close the connection request.
  4. The fourth wave: Client, after receipt of the FIN = N packets can be closed operation, but for the accuracy of the data, will be back an ack = N + 1 to the server, the server after receiving the message really disconnected, the client sends a confirmation message after a period of time, if have not received any information on the server side, is that the server connection has been closed, You can also close the client information.

Front and back interaction

After a TCP connection is successfully established, the front and back ends can communicate with each other through a protocol such as HTTP.

Back-end processing process:

I don’t know much about the processing details of the back end, if you are interested, you can learn more, but eventually it will be sent back to the front end in the form of a corresponding HTTP packet.

Browser caching (more on how to use it in a later optimization solution)

There are two caching strategies: strong caching and negotiated caching, which can significantly improve performance during HTTP front and back interactions.

Strong cache:

Strong caching is when the browser determines that the local cache has not expired, directly read the local cache, without making an HTTP request.

Negotiation cache: In the negotiation cache, the browser sends an HTTP request to the server to determine whether the file in the local cache of the browser is still in the unmodified state. If the file is not modified, the file is taken from the cache. If the file has been modified, the file needs to be resended to the browser.

Critical Render Path (CRP)

When we go through the web request process and get the page files from the server, how does the browser organize the HTML, CSS and JS files together and render them?

Building an object Model

First, the browser will parse HTML and CSS files to build the DOM (Document Object Model) and CSSOM (Cascading Style Sheet Object Model).

The browser receives the READ HTML file to build the DOM (Document Object Model) preview:

The browser receives the CSS file it reads to build the CSSOM (Cascading Style Sheet Object Model).

The construction process of these two object models takes time. You can open the Developer Tools TAB in Chrome to see the corresponding process time, as shown in the figure:

Rendering drawing

When the document object model and cascading Style Sheet object model are built, what you get is an object that describes two different aspects of the final rendered page: One is the content of the document presented, and the other is the style rules corresponding to the document object. The next step is to merge the two object models into a render tree. The render tree contains only the nodes visible to the render.

The steps of rendering are as follows:

  1. Start at the root node of the generated DOM tree and go down through each child node, ignoring all invisible nodes (JS script tags invisible, CSS hidden invisible) because invisible nodes are not in the rendered tree.
  2. Find the corresponding rule for each node in CSSOM and apply it.
  3. The layout phase, based on the resulting rendered trees, calculates their specific location in the device view, and this step outputs a “box model”.
  4. The drawing phase converts the specific drawing pattern of each node into pixels on the screen.

Optimization scheme

Request and response optimization

The DNS

In general, there are two things related to DNS in front-end optimization:

  • Reduce the number of DNS requests
  • Perform DNS Prefetch: DNS Prefetch

DNS Prefetch

Dns-prefetch is an attempt to resolve a domain name before a resource is requested. This could be a file to load later, or it could be the target of a link the user is trying to open. Domain name resolution and content loading are serial network operations, so this approach can reduce user waiting time and improve user experience.

Usage:

<link rel="dns-prefetch" href="https://example.com/">
Copy the code

Notes:

  1. Dns-prefetch is only valid for DNS looups on cross-domain domains, so avoid using it to point to your site or domain. This is because by the time the browser sees the prompt, the IP behind your site domain has been resolved.
  2. Use dns-prefetch with caution. Repeating DNS prefetch on multiple pages increases the number of repeated DNS queries.
  3. By default, the browser will Prefetch the domain name of the page that is not in the same domain as the current domain name (the domain name of the page being viewed) and cache the result. This is an implicit DNS Prefetch. If you want to Prefetch domains that are not present on the page, you use display DNS Prefetch.

Long HTTP connection

Short connection

In the initial version of the HTTP protocol, a TCP connection is broken every time an HTTP communication occurs.

For example, when you use a browser to view an HTMl page that contains multiple images, you send a request for access to the HTMl page’s resources, as well as requests for other resources that the HTMl page contains. Therefore, each request will cause unnecessary TCP connection establishment and disconnection, increasing the communication record overhead.

To solve this problem, some browsers use a non-standard Connection field on the request.

    Connection: keep-alive
Copy the code

This field requires the server not to close the TCP connection for reuse by other requests. The server also responds to this field.

A long connection

In January 1997, HTTP/1.1 was released, only six months after version 1.0. It further refines the HTTP protocol and is still the most popular version.

The biggest change in HTTP 1.1 is the introduction of HTTP Persistent Connections, which means TCP Connections are not closed by default, can be reused by multiple requests, and do not require a Connection: keep-alive declaration

The benefit of persistent connections is to reduce the overhead caused by repeated establishment and disconnection of TCP connections and reduce the load on the server. In addition, the reduced amount of overhead time allows HTTP requests and responses to end earlier, and Web pages display faster accordingly.

Pipeline mechanism

HTTP 1.1 also introduced Pipelining, which allows clients to send multiple requests simultaneously within the same TCP connection. This further improves the efficiency of the HTTP protocol.

Before sending a request, you need to wait and receive a response before sending the next request. With the advent of pipelining, the next request can be sent directly without waiting for a response. This allows you to send multiple requests in parallel at the same time, instead of waiting for a response one after the other, and to end the request faster with persistent connections than with individual connections. Pipelined technology is faster than durable connections. The more requests there are, the more significant the time difference becomes.

For example, a client needs to request two resources. The previous approach was to send A request and wait for A response from the server within the same TCP connection before sending B request. The pipe mechanism allows the browser to make A request and B request simultaneously, but the server responds to A request first and responds to B request after completion.

HTTP1.1 long connection disadvantage

Although HTTP version 1.1 allows the reuse of TCP connections, all data communications within the same TCP connection are sequential. The server does not proceed with the next response until it has processed one. If the first response is very slow, there will be many requests waiting in the queue. This is called “head-of-line blocking.”

To avoid this problem, there are only two ways:

  1. One is to reduce the number of requests
  2. The other is simultaneous multi-open persistent connection

This has led to many web optimization techniques, such as merging scripts and stylesheets, embedding images into CSS code, and so on.

HTTP2

In 2009, Google unveiled its own SPDY protocol to address the inefficiencies of HTTP/1.1.

multiplex

HTTP/2 reuse TCP connections. Within a connection, both the client and the browser can send multiple requests or responses at the same time, and they don’t have to be sequentially mapped, thus avoiding “queue head congestion.”

For example, in A TCP connection, the server receives A request and B request at the same time. It responds to A request first, but finds the processing time is too long, so it sends the processed part of A request, then responds to B request, and sends the rest of A request when it is complete.

This two-way, real-time communication is called Multiplexing.

Here’s an online example of comparing HTTP1 and HTTP2 resource loading: http2.akamai.com/demo

The data flow

Because HTTP/2 packets are sent out of sequence, consecutive packets within the same connection may be different responses. Therefore, the packet must be flagged to indicate which response it belongs to.

HTTP/2 refers to all packets per request or response as a stream. Each data stream has a unique number. When a packet is sent, it must be marked with a data stream ID to distinguish which data stream it belongs to. In addition, the ID of the data flow sent by the client is odd, and that of the server is even.

Halfway through the data stream, both the client and server can send a signal (RST_STREAM frame) to cancel the data stream. In version 1.1, the only way to cancel the data flow is to close the TCP connection. This means that HTTP/2 can cancel a request while keeping the TCP connection open for use by other requests.

The client can also specify the priority of the data flow. The higher the priority, the sooner the server will respond.

Header compression

The HTTP protocol does not carry status, and all information must be attached to each request. Therefore, many fields of the request are repeated, such as Cookie and User Agent, the same content must be attached to each request, which wastes a lot of bandwidth and affects speed.

HTTP/2 optimizes this with the introduction of Header Compression. On the one hand, headers are compressed using GZIP or COMPRESS before being sent. On the other hand, both the client and the server maintain a header table. All fields are stored in the table and an index number is generated. In the future, instead of sending the same field, only the index number is sent.

Server push

HTTP/2 allows the server to proactively send resources to the client, unrequested. This is called server push.

A common scenario is when a client requests a web page that contains many static resources. Under normal circumstances, the client must receive a web page, parse the HTML source code, find a static resource, and then send a static resource request. In fact, the server can anticipate that after the client requests the web page, it is likely to request static resources, so it actively sends these static resources along with the web page to the client.

Compress the transmitted data resources

Data compression is an important way to improve the performance of Web sites. For some files, compression rates of up to 70% can greatly reduce the need for bandwidth. As time goes on, compression algorithms become more efficient, and new compression algorithms are invented for both client and server applications.

HTTP response data is compressed

Compress JS and CSS

By compression, I mean compression that removes whitespace and the like, leaving the contents of the file unchanged.

Use Gzip to compress text

Active negotiation is used between the browser and the server. The browser sends the Accept-Encoding header, which contains the compression algorithms it supports and their respective priorities, and the server selects one of them, uses that algorithm to compress the body of the response message, and sends the Content-Encoding header to tell the browser which algorithm it has chosen. Since the content negotiation process selects the representation of the resource based on the Encoding type, at least accept-encoding must be included in the Vary header in the response; In this way, the cache server can cache different representations of the resource.

The following is an example of an HTTP packet requesting a response:

GET /encrypted-area HTTP/1.1 
Host: www.example.com 
Accept-Encoding: gzip, deflate
Copy the code
HTTP/1.1 200 OK 
Date: Tue, 27 Feb 2018 06:03:16 GMT 
Server: Apache/1.33.7. (Unix) (Red-Hat/Linux) 
Last-Modified: Wed, 08 Jan 2003 23:11:55 GMT 
Accept-Ranges: bytes 
Content-Length: 438 
Connection: close 
Content-Type: text/html; charset=UTF-8 
Content-Encoding: gzip
Copy the code

HTTP request data compression

Header data compression

The HTTP protocol does not carry status, and all information must be attached to each request. Therefore, many fields of the request are repeated, such as Cookie and User Agent, the same content must be attached to each request, which wastes a lot of bandwidth and affects speed.

HTTP/2 optimizes this with the introduction of Header Compression. On the one hand, headers are compressed using GZIP or COMPRESS before being sent. On the other hand, both the client and the server maintain a header table. All fields are stored in the table and an index number is generated. In the future, instead of sending the same field, only the index number is sent.

Request body data compression

In real Web projects, there are scenarios where the request body is very large, such as publishing a long blog, reporting network data for debugging, and so on. If the data can be compressed locally before submission, it can save network traffic and reduce transmission time. How to compress the HTTP request body, including how to compress the client side, how to decompress the server side two parts.

The three commonly used data compression formats are DEFLATE, ZLIB, and GZIP

Here is a simple example.

(1) Compress the request body data (client)

var rawBody = 'content=test'; 
var rawLen = rawBody.length; 
var bufBody = new Uint8Array(rawLen); 
for(var i = 0; i < rawLen; i++) { 
    bufBody[i] = rawBody.charCodeAt(i); 
} 
var format = 'gzip'; // gzip | deflate | deflate-raw 
var buf; 
switch(format) { 
    case 'gzip': 
        buf = window.pako.gzip(bufBody); 
        break; 
    case 'deflate': 
        buf = window.pako.deflate(bufBody); 
        break; 
    case 'deflate-raw': 
        buf = window.pako.deflateRaw(bufBody); 
        break; 
} 
var xhr = new XMLHttpRequest(); 
xhr.open('POST'.'/node/'); 
xhr.setRequestHeader('Content-Encoding', format); 
xhr.setRequestHeader('Content-Type'.'application/x-www-form-urlencoded; charset=UTF-8'); 
xhr.send(buf);
Copy the code

(2) Decompress the data in the request body in Node (server)

var http = require('http'); 
var zlib = require('zlib'); 
http.createServer(function (req, res) { 
    var zlibStream; 
    var encoding = req.headers['content-encoding']; 
    
    switch(encoding) { 
        case 'gzip': 
            zlibStream = zlib.createGunzip(); 
            break; 
        case 'deflate': 
            zlibStream = zlib.createInflate(); 
            break; 
        case 'deflate-raw': 
            zlibStream = zlib.createInflateRaw();
            break; 
    } 
    
    res.writeHead(200, {'Content-Type': 'text/plain'}); 
    req.pipe(zlibStream).pipe(res); 
}).listen(8361.'127.0.0.1');
Copy the code

HTTP cache

HTTP cache should be the front developing one of the most often contact caching mechanism, it can be divided into mandatory buffer cache and negotiation, the two biggest difference is that a judge a cache hit, the browser will need to ask the server to negotiate the cached information, and then determine whether need to the content of the response to the request. Let’s take a look at the specific mechanism of HTTP caching and the decision strategy of caching.

Mandatory cache

In the case of the forced cache, if the browser determines that the requested target resource is a valid hit, it can return the request response directly from the forced cache without any communication with the server.

Before we introduce the mandatory cache hit judgment, let’s first look at part of a response header:

access-control-allow-origin: * 
age: 734978 
content-length: 40830 
content-type: image/jpeg 
cache-control: max-age=31536000 
expires: Web, 14 Fed 2021 12:23:42 GMT
Copy the code

The two fields associated with mandatory caching are Expires and cache-control. Expires is a field declared in THE HTTP 1.0 protocol to control the timestamp of the cache expiration date. It is specified by the server and notified to the browser through the response header. The browser caches the response body with this field when it receives it.

If the browser makes the same resource request again, expires will be compared with the current local timestamp. If the local timestamp of the current request is less than Expires, the cached response has not expired and can be used directly without making a request to the server again. A new request to the server is allowed only if the cache expires when the local timestamp is greater than the Expires value.

Judging from the above mandatory cache is out of date mechanism of it is not hard to see, this means there is a big loophole, namely of a strong reliance on local timestamp, local time if the client and the server time out of sync, or the client time to take the initiative to change, then for the judgment of the cache expiration may be prevented from and expectations.

To address the limitations of expires, the cache-control field has been added since HTTP 1.1 to expand and improve the expires function. Cache-control sets the maxAge =31536000 attribute to control the duration of the response resource, which is a length of time in seconds, meaning that the resource is valid for 31536000 seconds after it is requested. This avoids the problem of server-side and client-side timestamps being out of sync. In addition, cache-control can be configured with a number of other property values to more accurately control the cache, as described below.

No – the cache and no – the store

Setting no-cache is not the literal meaning of not using the cache. It means to force the negotiated cache (as described later), that is, for each request made, the forced cache is not determined to be expired, but the server is directly negotiated to verify the validity of the cache, and if the cache is not expired, the local cache is used. Setting no-store disables any caching policy and requires a fresh response from the server every time the client requests it. No-cache and no-store are mutually exclusive and cannot be set at the same time.

Cache-control: no-store response header can disable caching.

If cache-control: no-cache or cache-control: max-age=0 is specified, the client can Cache resources. The client must reverify the validity of cached resources before using them. This means that an HTTP request is made each time, but the download of the HTTP response body can be skipped while the cached content is still valid.

Negotiate the cache

Before using the local cache, you need to send a GET request to the server to negotiate whether the local cache saved by the browser has expired.

This is usually based on the last time the requested resource was modified. To understand this, let’s look at an example: Suppose the client browser requests a JavaScript file resource from the server in manifest.js. In order for the resource to be requested again using the local cache, the response header that returns the image resource for the first time should contain a field named last-Modified. The attribute value of this field is the timestamp of the last modification of the JavaScript file. The key information for intercepting the request header and response header is as follows:

Request URL: http://localhost:3000/image.jpg 
Request Method: GET 
last-modified: Thu, 29 Apr 2021 03:09:28 GMT 
cache-control: no-cache
Copy the code

When we refresh the web page, because the JavaScript file uses the negotiated cache, the client browser cannot determine whether the local cache is expired, so it needs to send a GET request to the server to negotiate the cache validity. The request header for this GET request needs to include an ifModified-Since field with the same value as the last-Modified field in the last response header. After receiving the request, the server will compare the current modified timestamp of the requested resource with the value of the IF-Modified-Since field. If they are the same, the cache has not expired and the local cache can be used. Otherwise, the server will return a new file resource.

Request URL: http://localhost:3000/image.jpg 
Request Method: GET 
last-modified: Thu, 29 Apr 2021 03:09:28 GMT 
cache-control: no-cache
Copy the code

When we refresh the web page, because the JavaScript file uses the negotiated cache, the client browser cannot determine whether the local cache is expired, so it needs to send a GET request to the server to negotiate the cache validity. The request header for this GET request needs to include an ifModified-Since field with the same value as the last-Modified field in the last response header.

After receiving the request, the server will compare the current modified timestamp of the requested resource with the value of the IF-Modified-Since field. If they are the same, the cache has not expired and the local cache can be used. Otherwise, the server will return a new file resource.

// Request header for the second request
Request URL: http://localhost:3000/image.jpg 
Request Method: GET 
If-Modified-Since: Thu, 29 Apr 2021 03:09:28 GMT 
// Negotiate the cache of valid response headers
Status Code: 304 Not Modified
Copy the code

It is important to note here that the negotiation cache determines that the response status code is 304, which means that the cache valid response is redirected to the local cache. This is different from forced caching, which, if valid, will result in a response status code of 200 for the next request

The last – the shortage of the modifed

The last-modified negotiated cache works for most usage scenarios, but has two obvious drawbacks:

  • First of all, it only makes a judgment based on the last modified timestamp of the resource. Although the requested file resource is edited, the content does not change and the timestamp is updated. As a result, the judgment about the validity of the negotiated cache is validated as invalid and a complete resource request needs to be made again. This will undoubtedly cause a waste of network bandwidth resources, and prolong the user to obtain the target resource time.

  • Secondly, the time stamp unit of the file resource modification is identified as second. If the file modification speed is very fast, assuming that it is completed within several hundred milliseconds, then the validity of the cache verified by the time stamp mentioned above cannot be recognized.

In both cases, the server was unable to identify the actual update based on the timestamp of the resource modification, resulting in a re-request that used a cached Bug scenario.

Etag-based negotiation cache

To make up for the lack of time stamps, an ETag header has been added since the HTTP 1.1 specification, called an Entity Tag.

The content is a string generated by the server through hash operation for different resources. This string is similar to a file fingerprint. As long as the content encoding of a file is different, the corresponding ETag tag value will be different. Let’s look at an example of using ETag to negotiate a cache of image resources, with some of the key response headers following the first request.

Content-Type: image/jpeg 
ETag: "xxx" 
Last-Modified: Fri, 12 Jul 2021 18:30:00 GMT 
Content-Length: 9887
Copy the code

The above response header contains both the last-Modified time stamp and the ETag entity tag to verify the validity of the negotiated cache. ETag has a higher priority than last-Modified because it has a more accurate perception of file resource changes. When both exist, ETag takes precedence. When making a request for the image resource again, the system uses the value of the ETag field in the previous response header as the if-none-match field in the current request header and provides it to the server for cache validation. The key fields of the request header and response header are as follows:

Request header again:

If-Modified-Since: Fri, 12 Jul 2021 18:30:00 GMT 
If-None-Match: "xxx" // The last ETag value
Copy the code

Response header again:

Content-Type: image/jpeg ETag: "xxx" 
Last-Modified: Fri, 12 Jul 2021 18:30:00 GMT 
Content-Length: 9887
Copy the code

If the cache is valid, the response is redirected to the local cache by returning a 304 status code, so the content-Length field in the response header is 0.

The shortage of the ETag

Unlike in mandatory caches, where cache-control completely replaces Expires, in negotiated caches, ETag is not a last-modified alternative but a complement, because it still has some drawbacks.

  • On the one hand, the server is responsible for generating file resourcesETagAdditional computing overhead is required. If the size of the resource is large, the number is large, and the modification is frequent, then generateETagCan affect server performance.
  • On the other handETagThe generation of field value is divided into strong authentication and weak authentication. The strong authentication is generated according to the content of the resource, which can ensure that every byte is the same. Weak authentication is generated according to the part of the resource attribute values, grow faster but can’t ensure that each byte are the same, and in the server cluster scenario, also because of the inaccurate cache validation success rate and reduce the consultation, so the proper way is according to the specific resource usage scenarios cache invalidation of appropriate ways.
Sample caching decision

When using caching to optimize the performance experience, one problem is insurmountable: we want the cache to last as long as possible on the client side, but we also want it to be updated when resources change.

These are two mutually exclusive optimization claims. Using a forced cache and defining a long enough expiration time can make cached clients stay long, but because the forced cache has a higher priority than the negotiated cache, it is difficult to update. With a negotiated cache, although updates are guaranteed, frequent negotiated authentication with the server is certainly not as fast as using a forced cache. So how to balance the advantages of both?

We can disassemble the resources required by a website according to different types, and formulate corresponding caching strategies for different types of resources. The following HTML file resources are taken as an example:

<! DOCTYPEhtml>
<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.0">
    <title>HTTP Cache Policy</title>
    <link rel="stylesheet" href="style.css">
</head>

<body> 
    <img src="photo.jpg" alt="poto">
    <script src="script.js"></script>
</body>

</html>
Copy the code

The HTML file contains a JavaScript file script.js, a style sheet file style.css, and an image file photo-.jpg. To display the contents of the HTML, you need to load all the external files it contains. From this we can set them up as follows.

First, HTML belongs to the main file that contains other files. To ensure that it can be updated when its content changes, you should set it to a negotiated cache, that is, add a no-cache value to the cache-control field. The second is the image file, because the website basically changes the image file, and considering the number and size of the image file may cause a large amount of overhead on the client cache space, you can use the mandatory cache and the expiration time should not be too long, so you can set the value of the cache-control field to max-age=86400.

The next thing to consider is the style sheet file style.css. Since it is a text file, there may be irregular changes to the content, and you want to use mandatory caching to improve reuse efficiency. Therefore, you can consider adding the file fingerprint or version number to the naming of the style sheet file (for example, after adding the file fingerprint, the style sheet file name becomes style.51ad84f7.css), so that when the file is modified, different files will have different file fingerprints, that is, the file URL to be requested is different. Therefore, re-requests for resources are bound to occur.

Finally, there is the JavaScript script file, which can be set up similar to the stylesheet file.

As we can see from this example of a caching policy, using a combination of forced caching, negotiated caching, and file fingerprints or version numbers for different resources can achieve many things at once: timely updates, long cache expiration times, and control over where you can cache.

Code sample

Node. Js, for example

const http = require('http')
const fs = require('fs')
const url = require('url')
const etag = require('etag')

http.createServer((req, res) = > {
    console.log(req.method, req.url)
    const { pathname } = url.parse(req.url)
    if (pathname === '/') {
        const data = fs.readFileSync('./index.html')
        res.end(data)
    } else if (pathname === '/img/01.jpg') { // Strong cache-expires
        const data = fs.readFileSync('./img/01.jpg')
        res.writeHead(200, {
            // Disadvantages: Client time and server time may not be synchronized
            Expires: new Date('the 2021-08-01 00:46').toUTCString()
        })
        res.end(data)
    } else if (pathname === '/img/02.jpg') {  Cache-control: Max -age = 5
        const data = fs.readFileSync('./img/02.jpg')
        res.writeHead(200, {
            'Cache-Control': 'max-age=5' // Slide time, in seconds
        })
        res.end(data)
    } else if (pathname === '/img/03.jpg') {
        const { mtime } = fs.statSync('./img/03.jpg')

        const ifModifiedSince = req.headers['if-modified-since']

        if (ifModifiedSince === mtime.toUTCString()) {
            // Cache takes effect
            res.statusCode = 304
            res.end()
            return
        }

        const data = fs.readFileSync('./img/03.jpg')

        // Tell the client to use the negotiation cache for the resource
        // Before the client uses the cached data, ask the server whether the cache is valid
        // Server:
        // Valid: returns 304 and the client uses local cache resources
        // Invalid: returns the new resource data directly and the client uses it directly
        res.setHeader('Cache-Control'.'no-cache')
        // The server sends a field telling the client when the resource was updated
        res.setHeader('last-modified', mtime.toUTCString())
        res.end(data)
    } else if (pathname === '/img/04.jpg') {
        const data = fs.readFileSync('./img/04.jpg')
        // Generate a unique password stamp based on the file content
        const etagContent = etag(data)

        const ifNoneMatch = req.headers['if-none-match']

        if (ifNoneMatch === etagContent) {
            res.statusCode = 304
            res.end()
            return
        }

        // Tell the client to negotiate caching
        res.setHeader('Cache-Control'.'no-cache')
        // Send the content password stamp of the resource to the client
        res.setHeader('etag', etagContent)
        res.end(data)
    } else {
        res.statusCode = 404
        res.end()
    }
}).listen(3000.() = > {
    console.log('http://localhost:3000')})Copy the code

Rendering optimization

Critical render path optimization

The browser needs to complete the following steps from getting the HTML to finally displaying the content on the screen:

  1. Process the HTML tags and build the DOM tree.
  2. Process the CSS tags and build the CSSOM tree.
  3. Merge the DOM and CSSOM into a Render Tree.
  4. Layout according to the render tree to calculate the geometric information of each node.
  5. Draws the individual nodes to the screen.

The key render path optimization is to minimize the total time taken to perform steps 1 through 5 above, so that the user can see the first render as quickly as possible.

To do the first render as quickly as possible, we need to minimize the following three variables:

  • The number of critical resources.
  • Critical path length.
  • The number of key bytes.

The key resource is the resource that may prevent the first rendering of the page. For example, JavaScript and CSS are resources that can block critical rendering paths. The fewer these resources, the less effort the browser has to make, and the less CPU and other resources it consumes.

Similarly, the critical path length is influenced by a dependency graph between all critical resources and their byte size: some resources can only be downloaded after the previous resource has been processed, and the larger the resource, the more round trips the download takes.

Finally, the fewer key bytes the browser needs to download, the faster it can process the content and bring it to the screen. To reduce the number of bytes, we can reduce the number of resources (delete them or make them non-critical), and also compress and optimize resources to ensure that the transfer size is minimized.

Optimization of the DOM

In the critical Render path, the first step in building the Render Tree is to build the DOM, so let’s talk about how to make building the DOM faster.

The HTML file size should be as small as possible in order for the client to receive the full HTML as early as possible. There are many redundant characters in HTML, such as: JS comments, CSS comments, HTML comments, Spaces, newlines. Worse still, I’ve seen a lot of production HTML that contains a lot of discarded code, probably because of historical problems over time as the project gets bigger and bigger for a variety of reasons, but either way, it’s bad. For production HTML, you should remove any code that isn’t useful and keep your HTML files as thin as possible.

In summary, there are three ways to optimize HTML: Minify, gZIP, and HTTP Cache.

In essence, DOM optimization is about reducing the length of the critical path and the number of key bytes as much as possible.

Optimize the CSSOM

CSS is a necessary element for building a render tree, and JavaScript is often hindered by CSS when you first build a web page. Be sure to mark any non-essential CSS as non-critical resources (such as printing and other media queries), and be sure to minimize the number of critical CSS and minimize delivery times.

Block rendering CSS

CSS is a critical resource, and it’s not surprising that it will block critical rendering paths, but generally not all CSS resources are that “critical.”

For example: Some responsive CSS only works when the screen width is appropriate, and some CSS only works when the page is printed. The CSS doesn’t work when it doesn’t, so why make the browser wait for CSS resources we don’t need?

In this case, we should keep these non-critical CSS resources from blocking rendering.

<link href="style.css" rel="stylesheet"> 
<link href="print.css" rel="stylesheet" media="print"> 
<link href="other.css" rel="stylesheet" media="(min-width: 40em)"> 
<link href="portrait.css" rel="stylesheet" media="orientation:portrait">
Copy the code
  • The first declaration blocks rendering and applies in all cases.
  • The second declaration applies only when the web page is printed, so it does not block rendering the first time the web page loads in the browser.
  • Declaration provides a “media query” performed by the browser: When a condition is met, the browser blocks rendering until the stylesheet is downloaded and processed.
  • The last declaration has dynamic media queries that will be computed when the page loads. Depending on the orientation of the device when the page is loaded, portrait.css may or may not block rendering.

Finally, note that “blocking render” simply means whether the browser needs to pause the first rendering of a web page until the resource is ready. In either case, the browser will still download the CSS assets, but the resources that don’t block rendering will have a lower priority.

For best performance, you might consider inlining key CSS directly into HTML documents. This does not increase the number of round-trips in the critical path, and if implemented properly, can achieve “one round-trip” critical path length when only HTML is the resource blocking rendering.

Avoid using @import in CSS

We all know to avoid using @import to load CSS, and we don’t actually load CSS this way, but why?

This is because loading CSS with @import adds additional critical path length. Here’s an example:

<! doctypehtml>
<html>

<head>
  <meta charset="UTF-8">
  <title>Demos</title>
  <link rel="stylesheet" href="http://127.0.0.1:8887/style.css">
  <link rel="stylesheet" href="https://lib.baomitu.com/CSS-Mint/2.0.6/css-mint.min.css">
</head>

<body>
  <div class="cm-alert">Default alert</div>
</body>

</html>
Copy the code

The above code loads two CSS resources using the link tag. The two CSS resources are downloaded in parallel.

Now let’s use @import to load the resource as follows:

/* style.css */ 
@import url('https://lib.baomitu.com/CSS-Mint/2.0.6/css-mint.min.css'); 
body{
    background:red;
}
Copy the code
<! doctypehtml>
<html>
<head>
    <meta charset="UTF-8">
    <title>Demos</title>
    <link rel="stylesheet" href="http://127.0.0.1:8887/style.css">
</head>
<body>
    <div class="cm-alert">Default alert</div>
</body>
</html>
Copy the code

The link tag is used in the code to load one CSS, and the @import is used in the CSS file to load the other CSS.

As you can see, the two CSS loads become serial. After the previous CSS loads, you download the CSS resources imported using @import. This certainly leads to a longer total load time. As you can see from the figure above, the first drawing time is equal to the sum of the loading time of the two CSS resources.

Avoid @import to reduce the critical path length.

Optimize your use of JavaScript

All text resources should keep files as small as possible, and JavaScript is no exception. It also requires removing unused code, Minify, gZIP compression, and HTTP Cache.

  • Asynchronously loading JavaScript
  • Avoid synchronous requests
  • Delay parsing JavaScript
  • Avoid long running JavaScript
Use Defer to load the JavaScript lazily

Like CSS resources, JavaScript resources are key resources that block DOM construction. And JavaScript is blocked by CSS files.

tag, the browser cannot continue building the DOM. It must execute the script immediately. For external scripts

This leads to two important problems:

  • Scripts do not have access to the DOM elements below them, so scripts cannot add handlers to them, and so on.
  • If there is a clunky script at the top of the page, it will “block the page.” The user cannot see the contents of the page until the script is downloaded and executed
<p>. content before script...</p>

<script src="https://javascript.info/article/script-async-defer/long.js?speed=1"></script>

<! -- This isn't visible until the script loads -->
<p>. content after script...</p>
Copy the code

Here are some solutions. For example, we can place the script at the bottom of the page. At this point, it can access the elements above it without blocking the page display:

<body>. all content is above the script...<script src="https://javascript.info/article/script-async-defer/long.js?speed=1"></script>
</body>
Copy the code

But the solution is far from perfect. For example, the browser doesn’t notice the script (and can start downloading it) until the complete HTML document has been downloaded. For long HTML documents, this can cause significant delays.

For people with high-speed connections, it’s not a big deal. They don’t feel the delay. But there are still many parts of the world where people have slow Internet speeds and mobile Internet connections that are far from perfect.

Fortunately, there are two script features (attributes) that solve this problem for us: Defer and async.

The defer feature tells the browser not to wait for the script. Instead, the browser continues to process the HTML and build the DOM. Scripts are downloaded “in the background” and are not executed until the DOM build is complete.

This is the same example as above, but with the defer feature:

<p>. content before script...</p>

<script defer src="https://javascript.info/article/script-async-defer/long.js?speed=1"></script>

<! -- Visible immediately -->
<p>. content after script...</p>
Copy the code

In other words:

  • The script with the defer feature does not block the page.
  • The script with the defer feature always waits until the DOM is parsed.

The script with the defer feature maintains its relative order, just like regular scripts.

Let’s say we have two scripts with the defer feature: long.js first and small.js after.

<script defer src="https://javascript.info/article/script-async-defer/long.js"></script> <script defer src="https://javascript.info/article/script-async-defer/small.js"></script>
Copy the code

The browser scans the page for scripts and downloads them in parallel to improve performance. Therefore, in the example above, both scripts are downloaded in parallel. Small.js will probably be downloaded first.

… However, the defer feature, in addition to telling the browser “don’t block the page,” also ensures the relative order in which the script executes. Therefore, even if small.js is loaded first, it will not be executed until long.js is finished.

This can be useful when we need to load the JavaScript library first and then load the scripts that depend on it.

Use Async to lazily load JavaScript

The async feature is somewhat similar to defer. It also keeps scripts from blocking pages. But there are important behavioral differences.

The async feature means that the script is completely independent:

  • The browser does not block with the Async script (like defer).
  • Other scripts do not wait for async scripts to complete loading, and async scripts do not wait for other scripts.

In other words, async scripts are loaded in the background and run when they are ready. DOM and other scripts don’t wait for them, and they don’t wait for anything else. An async script is a completely separate script that is executed when the load is complete.

Resource loading optimization

Image lazy loading

What is lazy loading

First of all, imagine a scene. When browsing a website with rich content, such as the product list page of e-commerce, the program list of mainstream video websites, etc. Due to the limitation of screen size, only that part of the content in the window can be viewed at a time. Let the screen window show all parts of the entire page in turn.

Obviously, for the content beyond the first screen, especially pictures and videos, on the one hand, because the resource file is very large, if all the load, it is time-consuming and laborious, but also easy to block the rendering and cause lag; On the other hand, even when the load is complete, the user will not necessarily scroll to the entire page, and if the first screen fails to engage the user, the entire page will likely be closed.

In this case, in line with the principle of saving not waste, when the first time open the website, should try to load only the resources contained in the content of the first screen, and the first screen involved in the picture or video, can wait until the user scrolling window browse to load.

This is the logic behind the lazy load optimization strategy, which allows the page content to be presented to the user faster by delaying the loading of “non-critical” images and video resources. The “non-critical” resource here refers to the image or video resource that is not on the first screen. Compared to other resources such as text, script and so on, the size of the image resource should not be underestimated.

Implement image lazy loading

Refer to ruan Yifeng’s blog post intersectionobserver_API

Image optimization

  • Vectors are preferred where they are appropriate.
  • When using bitmaps, WebP is preferred for compatibility with unsupported browser scenarios.
  • Try to find the best quality Settings for the bitmap image format.
  • Compress the image file as necessary.
  • Responsive resource that provides multiple scaling dimensions for images.
  • Automate the engineering general image processing process as much as possible.

Build optimization

  • Webpack optimization
  • Resolution of the code
  • Code compression
  • Persistent cache
  • Monitoring and analysis
  • According to the need to load

conclusion

The content of this article involves a wide range of knowledge points, if there is a mistake also hope big guy to teach, if there are questions about the knowledge points, welcome to leave a message in the comment area.