TL; DR
- The packaging dimension can be defined based on HTTP Cache. Scripts with the same Cache cycle can be packed together as much as possible to maximize Cache utilization.
- Merge scattered small scripts to avoid triggering browser concurrent request limit, resource request serial, TTFB overlay waiting time;
- Note the packaged resource dependencies and the order in which resources are imported.
1. The introduction
Performance optimization covers a wide range of knowledge, which is also very complex. From loading performance to rendering performance to runtime performance, there is a lot to learn and practice at each point.
The optimization problem includes all aspects, and the optimization method also depends on the scene and specific problem. Therefore, this article is not intended to be a comprehensive overview, but rather a simple optimization of a previous business product (mainly DOMContentLoaded time), showing how to use Chrome Dev Tools to analyze the problem. Use strategies to shorten DOMContentLoaded time and speed up loading.
2. The DOMContentLoaded event
The W3C divides page loading into several stages. Similar to DOMContentLoaded, there are several DOM readStates that identify the loading state and stage of a page. The ones we see the most are interactive, complete (or Load), and DCL events in readState
Just a little bit about them. Browsers build the DOM based on HTML content and CSSOM based on CSS. When the two are built, they are merged into a Render Tree. When the DOM is built, the document.readyState state changes to interactive.
Once the Render Tree is built, you’ll enter the familiar Layout — >> Paint — >> Composite pipe.
But when the page contains Javascript, the process is a little different.
According to the HTML5 spec, DOM parsing is blocked when a browser parses a page because it can access the DOM in Javascript. At the same time, the CSSOM build blocks the execution of Javascript scripts to avoid races between CSS and Javascript. There is an exception, however, if the script is set to async, there is a difference, DCL firing does not need to wait for the async script to be executed.
That is:
- When the browser has finished parsing the document, the document state is marked as
interactive
. “DOM tree is ready”. - The browser fires when all the normal (neither defer nor Async) and defer scripts are executed and there are no blocking script styles left
DOMContentLoaded
Events. CSSOM is ready.
Or to simplify the above:
DOM construction can’t proceed until JavaScript is executed, and JavaScript can’t proceed until CSSOM is available. [1]
3. Troubleshoot problems
You can use Chrome Dev Tools to analyze the problem. In order to simplify the content, the following screenshots capture the access situation in slow 3G no cache mode. In order to maintain the online environment similar to (restore the browser’s same-source maximum request concurrency), the corresponding server is set up locally to place static resources. In the case of wifi, each time point is roughly equal to 8~9 times shorter.
First look at an overall waterfall
At the bottom you can see that the DCL is 17.00s (slow 3G).
P.S. page load times are also very long. The main reason is that after business expansion, the page contains too many resources, and some lazy loading and asynchronous rendering technologies are not used. There is also a lot of room for optimization in this part, but it is not discussed in this paper due to space.
There is an obvious request block in the page dCL-common.js. So what is common.js? It is simply a package of common script files in the project.
Because common.js is a synchronous script, the DCL is not triggered until it has been downloaded and executed. In contrast, the timelines of other scripts are quite different. The Timing pharse of common.js takes 11.44s, and the download takes 7.12s.
4. Analyze and diagnose
The most direct reason for a long download is that the file is too large. The package merge of common.js includes the following
'pkg/common.js': [
'static/js/bridge.js'.// Business base library
'static/js/zepto.min.js'.// Third-party libraries
'static/js/zepto.touch.min.js'.// Third-party libraries
'static/js/bluebird.core.min.js'.// Third-party libraries
'static/js/link.interceptor.js'.// Business base library
'static/js/global.js'.// Business base library
'static/js/felog.js'.// Business base library
'widget/utils/*.js' // Business tools components
]
Copy the code
Here, we found the following problems with packing this way:
4.1. File size
The most direct cause of long download: the file is too large.
Packing all these resources together results in a larger common.js file, 161KB originally, 52.5KB after gzip, and a single point blocking critical render paths. You can also find common.js to be the bottleneck in the Critical Request Chains section of audits.
4.2. HTTP Cache
Third party libraries such as Zepto/Bluebird are very stable resources that rarely change. Although there is a lot of code, using HTTP Cache can effectively avoid repeated downloads. Meanwhile, in order to prevent some files from going through HTTP Cache, we will add MD5 to static resources.
However, when these stable third-party libraries are packaged with some other files, the cache is invalidated because local changes to some of the files in the package lead to changes in the hash of the merged package.
For example, bridge.js and /utils/*.js are easy to iterate with the release, and packaging after iteration results in changes in common hash and invalidation of HTTP Cache. Zepto/Bluebird and other large resources, although unchanged, still need to be re-downloaded because they are packaged together. This is partly to blame for the significant decline in the performance of some loaded data each time a new release is released.
5. Implement optimization measures
Combined with the problems analyzed above, some simple and effective optimizations can be made.
5.1. Unpacking
Consider grouping and merging files according to how often they are updated. This can reduce the size of common.js and make better use of HTTP Cache for different types of resources.
Such as:
-
Basically immutable files are packaged as lib.js, mainly some third-party libraries. These files are almost immutable and very stable.
-
Package the most basic js that your project depends on as common.js, such as global.js and link.interceptor.js in this article. They are required for all parts of your project and are specific to your project. However, the update interval may be several versions.
-
Package the more frequently changing tool libraries in the project as util.js, where tools can theoretically be loaded asynchronously due to dependencies that do not run as a base. This part of the code changes the most frequently of the three.
'pkg/util.js': [
'widget/utils/*.js'].'pkg/common.js': [
'static/js/link.interceptor.js'.'static/js/global.js'.'static/js/felog.js'].'pkg/lib.js': [
'static/js/zepto.min.js'.'static/js/zepto.touch.min.js'.'static/js/bluebird.core.min.js'
]
Copy the code
5.2 Quene Delay
However, the DCL time hardly decreased after the separation.
This brings us to one of the original purposes of packaging: to reduce concurrency. After splitting common.js into three parts, we hit the same-domain TCP connection limit, and these four resources are queued by Chrome (white bar).
Queueing. The browser queues requests when:
- There are higher priority requests.
- There are already six TCP (Chrome) connections open for this origin, which is the limit. Applies to HTTP/1.0 and HTTP/1.1 only.
- The browser is briefly allocating space in the disk cache
We package and merge resources partly to reduce TCP round trips and avoid concurrent requests in the same domain. Therefore, when common.js is split, it should not be too detailed, otherwise too much of a good thing, forget the original intention.
It is also easy To find from network Waterfall that, due To the small size of most resources, the download Time is actually very short, and the Time is mainly in TTFB (Time To First Byte), which can be roughly understood as waiting for data To be returned by the server (more green is shown in the figure). So in addition to packaging the project dependencies lib.js/common.js/util.js, you can also consider packaging and merging some dependent component scripts.
The four scripts like the one in the figure above are all time-consuming on TTFB and on the same CDN, which can be packaged to reduce unnecessary concurrency. Package the key components that the first screen relies on:
'pkg/util.js': [
'widget/utils/*.js'].'pkg/common.js': [
'static/js/bridge.js'.'static/js/link.interceptor.js'.'static/js/global.js'.'static/js/felog.js'].'pkg/lib.js': [
'static/js/zepto.min.js'.'static/js/zepto.touch.min.js'.'static/js/bluebird.core.min.js'].'pkg/homewgt.js': [
'widget/home/**.js'.'widget/player/*.js',]Copy the code
The optimized DCL becomes 11.20s.
5.3 Sequence of Importing resources
Note that some packaging tools automatically analyze file dependencies and replace resource paths as files are packaged. For example, in HTML, quotes the static/js/zepto. Min. Js and static/js/bluebird. Core. Min. Js two resources, after packaging will build tools will automatically be replaced with HTML references in lib. Js. Therefore, pay attention to the loading sequence of packaged resources.
For example, the order of resources in the original HTML
<script type="text/javascript" src="//your.cdn.com/static/js/bridge.js"></script>
<script type="text/javascript" src="//your.cdn.com/static/js/zepto.min.js"></script>
<script type="text/javascript" src="//your.cdn.com/static/js/bluebird.core.min.js"></script>
<script type="text/javascript" src="//your.cdn.com/static/js/global.js"></script>
Copy the code
Global.js relies on Zepto.min.js, which is fine so far. But because of the package merge, the build tool automatically replaces the script file name. Due to the location of bridge.js, common.js is introduced before Lib.js after packaging. This causes global.js to be introduced and executed before Zepto.min.js, causing an error.
You can adjust the script order without affecting the original dependencies
<script type="text/javascript" src="//your.cdn.com/static/js/zepto.min.js"></script>
<script type="text/javascript" src="//your.cdn.com/static/js/bluebird.core.min.js"></script>
<script type="text/javascript" src="//your.cdn.com/static/js/bridge.js"></script>
<script type="text/javascript" src="//your.cdn.com/static/js/global.js"></script>
Copy the code
The output is as follows:
6. Verify the effect
Finally, the DCL time under slow 3G without cache is 11.19s, which is 34% lower than the initial 17.00s. (The decline rate of wifi is the same, and the time is about 1/8~1/9, close to 1s). At the same time, some static resources can make better use of HTTP Cache than before, saving bandwidth and reducing the number of static resources downloaded by users visiting the site after the launch of a new version.
7. Write at the end
It should be noted that there may be some “ground rules” for performance optimization, but there is absolutely no silver bullet. No matter how “basic and generic” optimization approaches are, or how “complex and targeted” optimization approaches are, they solve specific problems. Therefore, performance problems are often solved from a practical point of view, through the “troubleshooting problem -> analysis and diagnosis -> implementation optimization -> verification of the effect of the cycle.”
Also, one of the goals of improving performance is a better user experience. User experience is often a broad concept with many dimensions. Correspondingly, performance optimization should not only focus on a certain “indicator”, but also understand the meaning behind the product and user. Start from the problem, take data quantification, find a solution.
Create maximum value in a practical environment with limited resources and constraints. This is especially true for performance tuning.
The resources
- HTML5 spec: parse HTML (the end)
- HTML5 spec: current-document-readiness
- Deciphering the Critical Rendering Path
- Network Analysis Reference