参考文档为google developers中web性能部分的加载性能章节。大家可以不看这篇文章,直接去看官网文档。
官网地址
本文为一些总结和个人观点。
目的
目的当然是快。对于快,官方也给予了定义(RAIL),满足这个要求即可,避免过度优化。
RAIL 是一种以用户为中心的性能模型。是Response、Animation、Idle、Load四个单词的缩写。
- 以用户为中心;最终目标不是让您的网站在任何特定设备上都能运行很快,而是使用户满意。
- Response 立即响应用户;在 100 毫秒以内确认用户输入。
- Animation 设置动画或滚动时,在 10 毫秒以内生成帧。
- Idle 最大程度增加主线程的空闲时间。
- Load 持续吸引用户;在 1000 毫秒以内呈现交互内容。
本文只涉及Load部分:如果网站可以秒开,那就完美了。
关键渲染路径 CRP(Critical Rendering Path)
网站加载性能跟这个概念密切相关,这个概念是指当浏览器接收到html后,到完成第一次绘制到屏幕所需要的步骤。原文地址
- 构建DOM树(Constructing the DOM Tree)
- 构建CSSOM树 (Constructing the CSSOM Tree)
- 运行JS (Running JavaScript)
- 创建渲染树 (Creating the Render Tree)
- 生成布局 (Generating the Layout)
- 绘制 (Painting)
1. 构建DOM树
DOM(Document Object Model)树包含了页面的所有节点的对象。
HTML可以被部分执行,意思是不需要完整地构建完DOM树才展示到页面,可以一部分一部分展示。但是,CSS和JS会阻塞这个过程。
2. 构建CSSOM树
CSSOM(CSS Object Model)是包含了与DOM相关的样式对象。和DOM相似,但是包含了所有节点的样式信息,不管是明确声明的还是继承的(附:原文没提,还有浏览器默认的)。
CSS是阻塞渲染的资源,意思是在CSS资源被解析前,渲染树不会被创建。这是因为CSS的级联特性、后面加载的样式会覆盖前面的。
CSS同样会阻塞脚本执行,因为JS文件必须得等CSSOM构建完才能运行。
3. 运行JS
JS是阻塞解析(HTML)的资源,意思是解析HTML文档会被JS阻塞。
当解析器遇到script标签,会停止解析,去获取执行JS。
4. 创建渲染树
通过结合DOM和CSSOM,创建最终显示在页面上的渲染树(例如display为none的元素,就不会出现在渲染树中)。
5. 生成布局
这个我没看太懂,讲的是viewport大小,意思是整个视窗的大小,作为CSS样式的上下文。跟平时讲的layout有所不同。
6. 绘制
将可见元素(附:渲染树)转换成像素显示到屏幕上。
绘制的时间取决于DOM和样式的复杂程度。
文章最后的调试图片跟目前Chrome DevTools显示出入比较大,但是挨个展开event log中的task,基本是一致的。
测试工具
- Lighthouse
- PageSpeed Insights
- WebPageTest
- Pingdom
这几个工具都是可以出性能报告的。单个工具都不是十分准确,详尽的,要结合使用。
其中Lighthouse可以直接在DevTools中Audits面板中打开,下图是报告(低性能模式)
其中First Meaningful Paint(FMP)和Time to Interactive(TTI)是两个很重要的指标。
下图是WebPageTest出的报告:
总结
为什么总结要写在这里呢,因为事无巨细,官方文档写得方方面面涉及太多了。数了一下,细节点大概五六百个,所以先总结理清一下思路。
文章最后还介绍了webpack,webpack解决了大部分问题。
下面就按照这几个方面阐述(与官方文档顺序不一样)。
减少资源体积
文本
JS、HTML、CSS文件都属于文本资源。
1. 开发和生产代码分离
现代框架都提供build脚本构建生产版本代码。
2. 最小化
即删除注释、空格、换行等无用的东西。
3.源码压缩
使用UglifyJS压缩ES5代码。使用babel-minify或uglify-es压缩ES2015+代码。
4.传输压缩
即采用Gzip压缩传输文本。Brotli~q11更好。
5.减少库的使用
有些库很大的,譬如jQuery,压缩后也有100K左右,如果只用个选择器,完全可以用浏览器自带的querySelector等方法,或者使用jQuery核心库Sizzle。
6.Tree Shaking(删除未使用代码)
前提是使用ES6模块,webpack在打包时候会删除未使用的代码。
然后在引用代码的时候,按需引用。
例如
import * as utils from "../../utils/utils";
改成
import { simpleSort } from "../../utils/utils";
注意:
- babel会将ES6模块转成CommonJS模块,要关闭此功能。
{
"presets": [
["env", {
"modules": false
}]
]
}
特殊情况1(例lodash)
有些包不是按照ES6模块开发的,按照上面格式引用依然无用,譬如lodash
// This still pulls in all of lodash even if everything is configured right.
import { sortBy } from "lodash";
要解决此问题,有两种方式
- 改用
lodash-es
库
// This will only pull in the sortBy routine.
import sortBy from "lodash-es/sortBy";
- 安装
babel-plugin-lodash
插件
特殊情况2 (例moment.js)
moment库压缩后有223KB大小,其中170KB是语言包,不优化的话,会全部被打包到生产环境代码去。
要解决此问题,使用moment-locales-webpack-plugin
插件。
其他特殊情况
https://github.com/GoogleChromeLabs/webpack-libs-optimizations
列举了很多特殊情况(包括上面两种)
图片、视频
优化
一堆专业性的压缩图片技术和工具就不讲了,就是减少图片和视频体积(在不影响网页观看效果前提下)。
- 选择合适图片格式(例:从png 232K 到 jpg 42K)
- 删除元数据(即一些“无用信息”:相机、拍照日期、位置等,例:363K删到325K)
- 裁剪图片尺寸;提供展示缩略图;降低图片质量(227K到20K)
- 压缩图片(551K到133K)
gif动画转视频
gif动画非常大的,可以利用工具转换成视频,并设置自动播放。
例:将一个13.7MB的gif动画转换成了867K的mp4视频。
webm更好,但没有mp4兼容性好。
<video autoplay loop muted playsinline>
<source src="oneDoesNotSimply.webm" type="video/webm">
<source src="oneDoesNotSimply.mp4" type="video/mp4">
</video>
根据设备信息请求不同图片
srcset,和媒体查询类似,不同设备请求不同质量图片
<picture>
<source media="(min-width: 800px)" srcset="head.jpg, head-2x.jpg 2x">
<source media="(min-width: 450px)" srcset="head-small.jpg, head-small-2x.jpg 2x">
<img src="head-fb.jpg" srcset="head-fb-2x.jpg 2x" alt="a head carved out of wood">
</picture>
图像CDN
专业图像CDN站会帮你自动处理好图片,但收费。
字体
字体的优化也讲了很多,有格式问题,有Gzip压缩问题。
值得注意的是 unicode-range 描述符,可以指定加载部分子集(不知道要不要服务器支持)。
例如,您可以将 Awesome Font 系列拆分成拉丁文和日文子集,其中的每个子集将由浏览器根据需要下载:
@font-face {
font-family:'Awesome Font';
font-style: normal;
font-weight: 400;
src: local('Awesome Font'),
url('/fonts/awesome-l.woff2') format('woff2'),
url('/fonts/awesome-l.woff') format('woff'),
url('/fonts/awesome-l.ttf') format('truetype'),
url('/fonts/awesome-l.eot') format('embedded-opentype');
unicode-range:U+000-5FF; /* Latin glyphs */
}
@font-face {
font-family:'Awesome Font';
font-style: normal;
font-weight: 400;
src: local('Awesome Font'),
url('/fonts/awesome-jp.woff2') format('woff2'),
url('/fonts/awesome-jp.woff') format('woff'),
url('/fonts/awesome-jp.ttf') format('truetype'),
url('/fonts/awesome-jp.eot') format('embedded-opentype');
unicode-range:U+3000-9FFF, U+ff??; /* Japanese glyphs */
}
减少请求次数
这里的减少请求次数是指减少网络请求次数,所以缓存也算在这个优化里。
原因
1. 一个请求的生命周期
可以看到,一个请求要经过队列等待、DNS查找、初始化连接、SSL、发送请求、等待、下载等过程。
每一步都是需要时间和资源的。
2. Queueing(队列)
- 存在更高优先级的请求
- 此源已打开六个 TCP 连接,达到限值。 仅适用于 HTTP/1.0 和 HTTP/1.1
-
浏览器正在短暂分配磁盘缓存中的空间
这里的优先级需要讲一下,浏览器拥有自己的启发式算法,自动为各个资源评级,等级高的会优先去获取资源,低的会等待。看下图Priority列
浏览器用自己的启发式算法为各种资源分配了默认级别:
- 给了html highest级别,这是肯定的
- 给了css文件highest级别,这是因为css会阻碍渲染(后面会讲)
- 然后给了js文件high级别(图中有一些low的js文件是懒加载的)
- 其他一些不重要的资源例如图片都给了low级别
3. 网络连接
http连接是基于TCP的,关于http和TCP及网络带宽、延迟对加载网页影响,可以看下面这篇文章。
Primer on Web Performance
我的个人理解是,tcp连接是一个复杂的过程,要消耗性能和资源,而http/1的底层设计更是让这个过程很耗时,多个请求要建立多次tcp连接(关于http/2是如何优化的后面会讲)。而且建立一个tcp连接后,会根据资源的大小,往返多次。
4. 综上所述
请求次数越少越好,力求主页加载的都是自己所需要的资源。
合并文本资源
就是将多个css文件合并成一个,多个js文件合并成一个,webpack打包工具就可以做到。
合并图片
即将多张背景图片合并成一张,然后通过背景图片位置显示不同图片。
JS位置
关于如何加载JavaScript,又是一个复杂的,历史性的问题,可以参照这篇文章Deep dive into the murky waters of script loading。
其复杂性在于,要想达到如下目的,很麻烦。
- 不阻塞渲染
- 不导致重复下载
- 兼容各浏览器
简单的解决方案是
1. 将script引用标签放在body底部
2. 如果有js脚本必须先执行且代码较少,可以直接硬写到html中,这样可以减少请求次数
缓存
- Memory Cache
- Service Worker Cache
- HTTP Cache (Disk Cache)
- Push Cache
一个请求会按顺序读取这几层缓存,一旦命中就返回资源。
- Memory Cache
不是我们可以控制的,浏览器会缓存一些图片等资源到内存中,再次遇到请求直接返回,一个例外是preload的资源也会放在Memory Cache中。 - Service Worker Cache
可以说给我们开了一个口子,去定制缓存的行为,完成一些用http缓存完全做不到的事情。Service Worker完全没有任何规则,就看我们怎么去编码控制了,自由度极高,缓存的地方也自由:LocalStorage、IndexedDB、FilesSystem、Caches Api。每个存储类型都有各自的特点及使用场景。
官方列举了一些建议规则及实现代码及其使用场景- Cache only (仅缓存)
- Network only(仅网络)
- Cache,falling back to network(优先缓存,网络备用)
- Cache & network race(缓存和网络竞赛)
- Network falling back to cache(优先网络,缓存备用)
- Cache then network(优先缓存,同时去请求网络(下次请求可用))
- Generic fallback(常规回退,譬如自定义一个字符串)
- ServiceWorker-side templating(模板)
- HTTP Cahce(Disk Cache)
很古老的东西了 - Push Cache
这是结合HTTP/2协议的缓存,简言之就是,请求index.html时候,服务器不仅返回了index.html,也推送了其他关键资源,这些资源被放在Push Cache中。
HTTP/2
HTTP/2协议是好东西,如果有条件使用一定要使用,以二进制分帧为核心从底层解决了HTTP/1的一些令人诟病的问题。使用了HTTP2,上面的一些优化手段可以舍弃了(主要是代码合并方面的,例如级联文件、图片精灵)
1. 连接复用
在 HTTP/1.x 中,如果客户端要想发起多个并行请求以提升性能,则必须使用多个 TCP 连接,不仅耗性能,浪费资源,而且会造成队首阻塞,效率低下。
HTTP/2 利用二进制分帧层解决了这个问题,可以在一个TCP连接中并行发送多个请求和响应,应用速度更快、开发更简单、部署成本更低。
2. 标头压缩
HTTP/2 使用HPACK压缩请求和响应标头(header),极大减少了标头大小(减小85%-88%)。
我们发现,仅仅由于标头压缩,页面加载时间就减少了 45 - 1142 毫秒
3. 服务器推送
一般打开一个网站,是先向服务器请求一个index.html文件,然后解析html,再去加载js、css等资源,这是两个串行过程。
而HTTP/2提供了服务器推送技术,打开一个网站,服务器知道你需要什么资源,一起把index.html及js、css等静态资源全部推送给你,当浏览器解析完html,请求js和css时候,直接在缓存中取就可以了。减少了一次网络请求。
有好处就有弊端:处理的不好的话,重复发送资源,浪费了缓存这一功能。
优化关键渲染路径
关键渲染路径 CRP(Critical Rendering Path)的概念在最开始就讲了。
下面说一下各个过程简介及调试方法(因官网的配图都比较老了)。
文件列表
index.html
<!DOCTYPE html>
<html>
<head>
<title>Understanding the Critical Rendering Path</title>
<link rel="stylesheet" href="main.css">
</head>
<body>
<header>
<h1>Understanding the Critical Rendering Path</h1>
</header>
<main>
<h2>Introduction</h2>
<p>Lorem ipsum dolor sit amet</p>
<div>
<img src="./images/panda.jpeg" />
</div>
</main>
<footer>
<small>Copyright 2017</small>
</footer>
<script src="main.js"></script>
</body>
</html>
main.css
body { font-size: 18px; }
header { color: plum; }
h1 { font-size: 28px; }
main { color: firebrick; }
h2 { font-size: 20px; }
footer { display: none; }
main.js
function abc() {
console.log("just a simple js");
}
abc();
panda.jpeg
1.构建对象模型(DOM)
Bytes -> Characters -> Tokens -> Nodes -> DOM
四个过程
- 根据编码将字节转换成字符串
- 令牌化:将字符串转换成各个html标签
- 词法分析:将标签转换成节点对象(包含属性和规则)
- 构建DOM树
调试查看(找到Parse HTML)
打开Chrome DevTools -> Audits,评估完点击 View Trace,就会进入Perfermance面板。
主线程Main中的火焰图和Event Log中是一一对应的。
我们一个一个展开EventLog中的Task,找到Parse HTML,看到解析HTML,构建DOM树,话费了1.1ms。
提示:totaltime是这一过程总时间,selftime是自身执行时间,totaltime - selftime就是其调用的子任务的执行时间。
2. 构建CSS对象模型(CSSOM)
Bytes -> Characters -> Tokens -> Nodes -> DOM
与构建DOM类似,不过产出是CSSOM。
小细节:浏览器会有一组默认样式(User Agent),也可以说会有一个默认CSSOM树,解析样式其实是去替换默认样式。
调试查看(找到Parse Stylesheet)
这一个我调试验证了很久,我觉得官网说的是对应DevTools中Recalculate Style过程,并不对,应该是Parse Stylesheet,理由如下:
- CSSOM构建会阻塞js执行,main.js必须等到CSSOM构建好,才能执行。这一点在Recalculate Style上并不能体现,反而在Parse Stylesheet上得到了证明:main.js百分百在Parse Stylesheet后执行
- DOM解析构建对应DevTools中的Parse HTML,是蓝色的。Parse Stylesheet也是蓝色的,有理由相信就是CSSOM的解析构建。
官方上这样写,可能是之前的DevTools并没有如此细分。网上查不到相关信息,所以个人理解可能也不对。
3. 构建渲染树(Render tree)
前面根据HTML构建了完整的DOM树、根据CSS构建了完整的CSSOM树。
这是两个独立的树,根据这两个树,合并计算出渲染树。
这个过程大概是这样:
- 遍历DOM树中可见节点(不可见节点有:script、meta标签等;display:none的元素等,注意visibiility:hidden的不同,会依然占据布局)
- 对每个可见节点,找到匹配的CSSOM规则并应用。
- 发射可见节点,连同其内容和计算的样式(我的理解就是发射渲染树)
调试查看(找到Recalculate Style)
同上,跟官网有出入。而且我认为构建渲染树对应Recalculate Style理由如下:
-
如果在main.js中调用getComputedStyle获取样式,会强制提前执行Recalculate Style过程,所以Recalculate Style应该就是构建渲染树。
Recalculate Style还列出了相关信息:上图是影响了8个元素
4. 布局(layout或者叫reflow)
根据渲染树和视口(viewport),计算所有可见元素相对视口的绝对像素位置和像素尺寸,输出盒模型(box model)。
调试查看(找到Layout)
Layout也列出了相关信息:上图是需要布局11个元素
5. 绘制(painting或者叫rasterizing)
就是输出到屏幕了
调试查看(找到Update Layer Tree、Paint、Composite Layers、Rasterize Paint)
- 其中Update Layer Tree我也不知道是归于Layout还是归于painting好。
- 拿剪贴画为例,要做一个三层的剪贴画,先在一张纸上画上草地,然后剪一个小人并画上衣服,然后剪一个鼻子涂上红色,最后一个个贴上去。Paint就是画画的过程,Composite Layers就是贴的过程,Rasterize Paint就是最后成果图输出到屏幕了。(剪多大,贴在哪儿就是前面Layout的过程了)
- 注意这里的Rasterize Paint已经不再主线程(Main)里了。
优化关键路径
优化关键路径就是缩短1-5消耗的总时间。尽快将网页渲染到屏幕上。还能缩短首次渲染后屏幕刷新的事件,为交互式内容实现更高刷新率(不懂)。
CSS会阻塞渲染
由上可知,同时具有 DOM 和 CSSOM 才能构建渲染树,所以要精简CSS,并尽快提供。而且要用好媒体查询,例如指定打印的css文件就不会阻塞渲染。
验证
为了验证CSS会阻塞渲染,需要让css文件延迟响应,我搭建了一个Go服务,延迟2秒返回css文件。
wiki.go
package main
import (
"fmt"
"io/ioutil"
"net/http"
"path/filepath"
"time"
)
func main() {
// 延迟css文件
http.HandleFunc("/main.css", func(w http.ResponseWriter, r *http.Request) {
time.Sleep(2000 * time.Millisecond)
// fmt.Fprintf(w, "Hello, you've requested: %s\n", r.URL.Path)
absPath, _ := filepath.Abs("./static/main.css")
maincss, _ := ioutil.ReadFile(absPath)
w.Header().Set("Content-Type", "text/css")
fmt.Fprintf(w, string(maincss))
})
// 其他文件直接读取
http.Handle("/", http.FileServer(http.Dir("static/")))
http.ListenAndServe(":80", nil)
}
index.html
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link href="main.css" rel="stylesheet">
<title>Critical Path: Script</title>
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<footer>footer</footer>
</body>
</html>
新打开一个浏览器标签页,打开localhost,会发现页面空白2秒后才显示内容。
打开performance调试,会发现有2秒的空白时间在等待css文件,然后才进行执行脚本,页面渲染。
使用JS添加交互
引用:JavaScript 允许我们修改网页的方方面面:内容、样式以及它如何响应用户交互。 不过,JavaScript 也会阻止 DOM 构建和延缓网页渲染。 为了实现最佳性能,可以让您的 JavaScript 异步执行,并去除关键渲染路径中任何不必要的 JavaScript。
- JavaScript 可以查询和修改 DOM 与 CSSOM
- JavaScript 执行会阻止 CSSOM
- 除非将 JavaScript 显式声明为异步,否则它会阻止构建 DOM
1. JavaScript 可以查询和修改 DOM 与 CSSOM
这一条不用讲什么了
2. JavaScript 执行会阻塞 CSSOM
这一句我验证时候绕了很多弯弯,开始我猜测是JavaScript的加载执行结束,才会进行CSSOM构建,多方验证失败(在服务端延迟2秒返回js文件,浏览器会先渲染js之前的,加载执行js后进行第二次渲染)。后来感觉作者的意思应该很直白,就是:JavaScript执行和CSSOM都是在主线程执行的,JavaScript执行时候,CSSOM构建肯定不能执行,就这么简单。
index.html
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link href="main.css" rel="stylesheet">
<title>Critical Path: Script</title>
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<script>
var str = "";
for(let i=0; i < 10000000; i++) {
str += "a";
}
</script>
<footer>footer</footer>
</body>
</html>
可以看到,我写了一个执行长达941ms的脚本,在这段脚本执行结束后,才进行了CSSOM构建。
3. 除非将 JavaScript 显式声明为异步,否则它会阻止构建 DOM。
这个意思是,html中遇到js,将会停止解析DOM,等待js下载执行完毕继续解析后面的DOM。
在Go服务器中将main.js延迟20ms返回,查看performance。
index.html
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link href="main.css" rel="stylesheet">
<title>Critical Path: Script</title>
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<script src="main.js"></script>
<footer>footer</footer>
</body>
</html>
可以很明显看到html被分成2段执行了,js后面的页面等待js加载执行完毕才解析。
将<script src="main.js"></script>
改成<script src="main.js" async></script>
后
加上async后,浏览器不等待js文件加载执行,就解析完了dom。
4. JavaScript 执行将暂停,直至 CSSOM 就绪
参照上面的CSS会阻塞渲染
配置,JS会等待CSS加载解析完毕,才会执行。
同样在Go服务器将css延迟2秒返回
这里讲几个注意点
- 4和5肯定是在6之前执行的。6和8其实并不一定严格按顺序执行,如果你在6里读取了元素样式,那么8就会在6之前执行,因为浏览器必须计算完样式(Recalculate Stylesheet),js才能获取正确的样式
例如在main.js
中加段代码:
var span = document.getElementsByTagName('span')[0];
span.textContent = 'interactive'; // change DOM text content
var spanStyle = getComputedStyle(span, null);
console.log(spanStyle.display);
可以看到,因为js读取了样式,导致了浏览器立即计算样式返回给js。
这个在网站运行性能里有详细讲解,并且如果代码写得不好,会导致反复计算样式,性能奇差。
- 接上条,如果读取了元素的尺寸相关,还会触发布局(Layout)
加上代码,读取了元素高度
var span = document.getElementsByTagName('span')[0];
console.log(span.clientHeight);
- style和getComputedStyle的区别
var span = document.getElementsByTagName('span')[0];
span.textContent = 'interactive'; // change DOM text content
console.log(span.style.display)
这段代码并不会触发浏览器计算样式,猜测是因为这样写(这个属性)是读取DOM属性。
5. 将js声明成异步(async)
<script src="main.js" async></script>
这样就不会阻塞构建DOM了,不过不能保证js的执行顺序,而且各个浏览器实现的不一致。
延迟加载(懒加载)、预加载
大概分了几种情况
- 针对图片视频,可见范围外的不加载
- 代码拆分,首屏只加载必须的资源,发生路由跳转时候加载其他资源
- 首屏加载必须的资源后,主动预加载一些其他资源,优化后续体验
1. 延迟加载(懒加载)图片、视频
初始只加载屏幕内的图片、视频,其他用占位图片代替。待用户操作(滚动页面)到相关位置,再去获取图片。
实现方式
a. 使用Intersection Observer(最优)
b. 使用Intersection Observer polyfill
npm install intersection-observer
c. 使用事件处理程序(兼容性最好的方法)
利用scroll、resize事件;利用getBoundingClientRect方法与orientationchange事件
d. 使用库
- lazysizes
- lozad.js
- blazy
- yall.js
-
react-lazyload
各个库自行研究吧,其中lozad.js是只使用Intersection Observer的库,性能最好,兼容性不好。
CSS中的图片
CSS中的图片有一个特点:浏览会计算需要哪些背景图,才会去加载,所以可以生命两种样式,设置不同的背景图片,通过切换样式来达到延迟加载
视频
视频需要设定preload="none"
属性阻止加载视频资源,设定poster="one-does-not-simply-placeholder.jpg"
属性来指定占位图片。
视频自动播放
延迟加载视频,又要像gif那样自动播放。
设定autoplay后,通过poster属性设定占位符,data-src设定真实资源,需要时候将src改成data-src。
JavaScript 代码拆分和延迟加载
这主要牵扯到工程化,一般工程化会把单页面应用打包成一个js文件,这样首屏消耗是很高的,可以利用代码拆分让首屏只加载必须的资源。
- 动态import
onShowCommentsClick(() => {
import('./comments').then((comments) => {
comments.renderComments();
});
});
这样Webpack就会将comments组件单独打包,首屏不会加载,交互时候才加载
- 单页面应用:按路由拆分代码
跟动态import类似,不过是路由级别的拆分 - 多页面应用
按照Webpack配置不同的入口文件
// webpack.config.js
module.exports = {
entry: {
home: './src/Home/index.js',
article: './src/Article/index.js',
profile: './src/Profile/index.js'
},
};
3. 预加载
这主要是优化交互体验了,跟首屏关系不大,预先加载一些即将使用的资源(譬如代码拆分后的首屏外的其他资源),后续操作就不会卡顿了。
资源优先级
上面讲队列的时候稍微提到了下。
浏览器加载资源会有不同优先级,查看方法:DevTools -> Network,右击表格的标题栏,勾选Priority选项:
浏览器会采用启发式算法,对不同资源有不同的优先级。因为网络连接是有限的,所以会为优先级高的资源请求,剩下的排队等待。
可以通过如下方式改变一些行为:
1. 预加载 preload
<link rel="preload" as="script" href="super-important.js">
<link rel="preload" as="style" href="critical.css">
preload可以提高资源的优先级,使用场景有:字体的加载优先级是很低的,可以通过preload来提高;preload可以加载CSS或者JS中定义使用的资源;html中引用css会阻碍渲染,可以通过preload来避免;
2. 预连接 preconnect
<link rel="preconnect" href="https://example.com">
一个请求要经过很多步骤,preconnect可以预先进行一些步骤:DNS 解析,TLS 协商,TCP 握手。大大改善了一些情况下的延迟问题。
也可以使用dns-prefetch,仅仅进行DNS查询。
3. 预提取 prefetch
这个跟preload很容易弄混淆,prefetch是去预提取一些资源到缓存中,以后会用到。
4. 预渲染 prerender
和prefetch相似,但是prerender会在后台渲染页面。(不知道怎么测试,我测试了好像没什么效果)
页面绘制
这一部分不单单是针对加载页面了,关于js执行效率和页面渲染效率等,打算另开一个文章写。
其他
性能API
Navigation Timing和Resource Timing帮我们收集性能相关信息。
使用这些API有如下特点
- 数据化:不用再在DevTools中一点点调试了,可以直接读取相关数据,做出自己的性能分析报告(很多性能测试工具就是基于此API);
- 更准确:打开DevTools调试其实也是消耗性能的,加载比直接打开网页要慢
- 可以收集用户真实的数据:直接在客户端收集信息后上传服务器,统计分析
Navigation Timing
performance.getEntriesByType("navigation");
导航时间线收集HTML文档性能指标
Resource Timing
资源时间线收集了文档依赖的各种资源的性能指标
performance.getEntriesByType("resource");
其他相关用法
- getEntriesByName:收集某个资源的性能指标
- getEntries:收集所有性能指标
- PerformanceObserver:自己循环遍历性能指标,会带来些问题,可以实现这个对象实例来遍历
- navigator.sendBeacon:如果想将用户性能指标数据发给服务器,可以写在unload事件中,但是一般写法(ajax,fetch等)会阻塞浏览器。可以使用navigator.sendBeacon避免这个问题。
新的时间API:performance.now
相关文档
上面的时间线中的时间都是通过这个方法处理处理的。
背景
之前处理时间一般都是用Date相关方法。有很多缺陷
- 不够精确,只能到毫秒数
- 不仅不够精确,还不准(受制于系统时钟偏移)
例如以前自己用Date获取时刻相减少,提示一段代码执行时间,发现,有时候执行16ms,有时候执行60多ms,显然是不对的。(当然也不会偏差的过分)。
所以有了新规范,目前是High Resolution Time Level 2。
具体实现就是通过实现performance相关属性和方法。
使用
performance.now(); // 7219725.014999989
输出了一个相对于初始时间的亚毫秒数(sub-millisecond,其实就是高精度的毫秒数)。
初始时间
可以通过
performance.timeOrigin // 1558493857670.848
获取。这个属性值,可以是浏览上下文创建时间,可以是新文档对象创建时间,可以是worker创建时间。不用关心初始时间,只是个参照而已。性能分析分析的都是时间段。
requestAnimationFrame
以前写动画都是利用setTimeout来处理的,有诸多弊端,性能也不好。这个方法可以改善这个问题。