关于前端的性能优化,主要从 URL
输入到页面展示的整个过程来讨论。
先来回顾一下从 URL 输入到页面显示,经历了什么过程?
'✔' 代表该过程在前端可进行优化
- 浏览器地址栏输入时联想提示
URL
解析,URL
为了在不同协议不同传输机制都可以安全的运送信息,采用的字符都是符合ASCII
集的。若有非ASCII
的字符(中文等),就会先通过转义处理- 客户端(浏览器)缓存机制 ✔
DNS (Domain Name System)
解析 ✔- 浏览器建立一条与
web
服务器的TCP
连接(TCP
三次握手) ✔ - 浏览器向服务器发送一条
http
请求报文 ✔ - 服务器向浏览器回送一条
http
响应报文 ✔ - 浏览器判断缓存 ✔
- 浏览器接收、解析、渲染资源 ✔
- 关闭连接(
TCP
四次握手)
优化之浏览器缓存机制
为了减少客户端(浏览器)向服务端请求次数和带宽,可借助浏览器缓存机制进行优化处理,进而提升网页的访问速度
浏览器缓存也包含很多种类:
- 本地缓存
- localStorage
- sessionStorage
- indexedDB
- cookie
- Service Worker
- 什么是 Service Worker
- HTTP 缓存
- 优先级底
- 协商缓存不管是否命中都要和服务器端发生交互
- 响应头控制字段:
Last-Modified && If-Modified-Since
、ETag && If-None-Match
- 优先级高于协商缓存
- 浏览器一旦命中强缓存,直接使用缓存,不会和服务器发生交互
- 响应头控制字段:
Expires
、Cache-Control
- 强缓存
- 协商缓存
优化之 DNS
预解析
DNS(Domain Name System)
,即域名系统,就是根据域名查出 IP
地址。查询域名对应的 IP
地址需要时间,前端能做的就是减少域名解析的时间,DNS
预解析就可以做到这一点。
X-DNS-Prefetch-Control
头控制着浏览器的 DNS
预读取功能。DNS
预读取是一项使浏览器主动去执行域名解析的功能,其范围包括文档的所有链接,无论是图片、CSS
、还是 JavaScript
等其他用户能够点击的 URL
。
因为预读取会在后台执行,所以 DNS
很可能在链接对应的东西出现之前就已经解析完毕。这能够减少用户点击链接时的延迟。
代码实现如下:
js
<!-- 打开和关闭 DNS 预读取 -->
<meta http-equiv="x-dns-prefetch-control" content="on">
<!-- 强制查询特定主机名 -->
<!-- 通过使用 rel 属性值为 link type 中的 dns-prefetch 的 <link> 标签来对特定域名进行预读取 -->
<link rel="dns-prefetch" href="//www.domain.com/">
加载静态资源优化原则
- 减少首屏内容大小
- 减少
HPPT
请求次数 - 减少资源包体积大小
- 异步加载
- 利用浏览器缓存
优化之减少首屏内容大小
如果所需的数据量超出了初始拥塞窗口的限制(通常是 14.6kB
压缩后大小),系统就需要在服务器和用户的浏览器之间进行更多次的往返。如果用户使用的是延迟时间较长的网络(如偏远地区网络),该问题可能会显著拖慢网页加载速度
为提高网页加载速度,请限制用于呈现网页首屏内容的数据(HTML
标签、图片、CSS
和 JavaScript
)的大小
优化之减少资源包体积大小(启用压缩功能)
所有现代浏览器都支持 gzip
压缩,启用 gzip
压缩可大幅缩减传输资源大小,从而缩短资源下载时间,减少首次白屏时间,提升用户体验
zip
像是一个打包器,能把多个多件打包压缩到一个.zip
文件包中gzip(GNU zip)
一次只对一个文件压缩
zip
和 gzip(gz)
不兼容
gzip
对基于文本格式文件的压缩效果最好(如:CSS
、JavaScript
和 HTML
),在压缩较大文件时往往可实现高达 70-90%
的压缩率,对已经压缩过的资源(如:图片)进行 gzip
压缩处理,效果很不好。
所以我们应该启用 gzip
压缩,gzip
压缩需要在静态服务器做简单配置,具体可参考这里
优化之 JavaScript
异步加载资源
默认情况下,浏览器是同步加载 JavaScript
脚本,解析 html
过程中,遇到 <script>
标签就会停下来,等脚本下载、解析、执行完后,再继续向下解析渲染。
如果 js
文件体积比较大,下载时间就会很长,容易造成浏览器堵塞,浏览器页面会呈现出"白屏"效果,用户会感觉浏览器"卡死了",没有响应。浏览器允许脚本异步加载、执行。
js
<script src="path/to/home.js" defer></script>
<script src="path/to/home.js" async></script>
上面代码中,<script>
标签分别有 defer
和 async
属性,浏览器识别到这 2
个属性时 js
就会异步加载。也就是说,浏览器不会等待这个脚本下载、执行完毕后再向后执行,而是直接继续向后执行
defer
与 async
区别:
defer
:DOM
结构完全生成,以及其他脚本执行完成,才会执行(渲染完再执行)。有多个defer
脚本时,会按照页面出现的顺序依次加载、执行async
:一旦下载完成,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染(下载完就执行)。有多个async
脚本时,不能保证按照页面出现顺序加载、执行
按需加载
在访问的页面只加载需要的资源(js、css、html 等
),这样可以减少带宽,加快页面响应速度
js
const importModule = (url) => {
const script = document.createElement('script')
script.src = url
document.querySelector('body').appendChild(script)
}
importModule('path/to/home.js')
如上代码便实现了按需加载 js
功能,在需要的时候调用 importModule()
即可加载对应的 js
资源
当打包构建应用时,JavaScript
包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了
在实际工作中我们常用动态导入 import() 来实现按需加载,vuejs
项目中,我们通常以路由为维度,进行代码分割,进行按需加载。
js
// vuejs router 片段
{
path: '/about',
name: 'about',
component: () =>import('@/pages/About.vue'),
}
如上代码,就实现了按需加载对应页面资源,从而提升了首屏加载速度,提高了用户体验
减少代码体积
- 压缩
- 去除无用代码
避免 Long Task
Long Tasks 是指执行时间超过 50
毫秒的任务,这些 Long Tasks
在很长一段时间内独占 UI
线程并阻止其他关键任务执行(如:点击、输入行为),阻塞了主线程,严重影响用户体验。
对用户而言,任务耗时较长表现为页面卡顿(反应慢),所以应该避免 Long Tasks
任务
优化之 CSS
如上图所示,浏览器渲染的主要步骤可分为:
- 处理
HTML
标记并构建DOM
树 - 处理
CSS
标记并构建CSSOM
树 - 将
DOM
与CSSOM
合并成一个渲染树 - 根据渲染树来布局,以计算每个节点的几何信息
- 在多个层上绘制各个元素的内容、边框、颜色、图像等
- 合成,由于页面的各部分可能被绘制到多层上面,需要合成来确保按正确顺序绘制到屏幕上,以便正确渲染页面
内联首屏关键 CSS
在 <head>
标签 <style>
中,内联 CSS
能使浏览器开始渲染时间提前。
js
<style>
.main-container {
background-color: #ccc;
}
</style>
如上代码,这种方式并不适用于内联较大的 CSS
文件。因为我们要遵守【减少首屏内容大小】优化原则
异步加载 CSS
默认情况下,CSS
阻塞浏览器渲染,这意味着未加载完毕 CSS
资源之前,浏览器将不会渲染任何已处理的内容。
js
DOM-tree(html) + CSSOM-tree(css) = Render-tree
从上面的公式可知,HTML
和 CSS
都是阻塞渲染的资源,HTML 是必不可少的,因为它承载着网页的内容,但 CSS 的必要性相对来说没有那么重(首屏关键 CSS 除外)。我们可以采用非关键性 CSS 异步加载的方式,来加快渲染树的构建。
js
<link rel="preload" href="style/bigger.css" as="style" onload="this.rel='stylesheet'">
如上代码,在支持 rel="preload"
属性的浏览器中,rel
属性会让浏览器获取样式,但加载完样式不会应用它。所以需要 onload="this.rel='stylesheet'"
,也就是加载完后应用 CSS
。
注:preload 有兼容性问题,可在这里查看
js
<!-- media 属性可以为其它任意值 -->
<link rel="stylesheet" href="style/bigger.css" media="backup" onload="this.media = 'all'">
如上代码,通过 <link>
标签更改 media="backup"
属性,浏览器会异步加载该样式但不会去应用,也就是说不会阻塞渲染树的构建,从而加快浏览器渲染。
注意:加载完后通过 onload="this.media = 'all'"
,将 media
属性值设置为 all
,让浏览器后续解析、渲染。 二者区别: rel="preload"
比 media="backup"
加载优先级要高
减少重排、重绘
关于影响 CSS
重排、重绘的属性有很多,具体可参考这里
js
DOM-tree(html) |
+ | => Render-tree => Layout(布局) => Paint(绘制) => Composite(合成)
CSSOM-tree(css)
如上为浏览器渲染关键步骤
-
减少重排(Layout),又叫回流
如果改变了元素的几何属性(如:
width、height、margin
等等),影响页面布局,浏览器就会重新检查所有元素,然后自动重排页面 -
减少重绘(Paint)
如果改变了不会影响页面布局的属性(如:
color、background-image 等等
),浏览器就跳过布局步骤,然后自动重绘页面
优化之图片
压缩图片
GIF
、PNG
和 JPEG
格式在整个互联网的图片流量中占 96%
。请确保部署到生产之前,对图片资源进行压缩处理,有个很好用的图片压缩网站 TinyPNG,当然也有 Google
开源的 Squoosh 压缩工具。SVG
矢量图属于 XML
文本类型,所以可以进行 gzip
压缩后再使用。
雪碧图(Sprite)
一种网页图片应用处理方式,将一个页面涉及到的所有小型图片或者图标都合并到一张大图里面,这样就只需要加载这个一个图片,而不是很多个图片了,这样就减少了很多 http
的请求,从而提高性能
js
/* 通过 background-position 属性控制图片显示位置,从而显示雪碧图中指定区域 */
.demo {
background: url('path/to/sprite.png') no-repeat;
background-position: 10px, 10px;
}
内嵌 base64
先将图片转为 base64
字符串,将其内嵌到页面中,这样可以减少 http
请求次数。但是图片转成 base64
体积会增大将近 1/3
,增大了 html、css
的体积,同时也失去了浏览器的缓存能力。一般用于首屏加载的小图标
图片懒加载
图片只加载视口内可看到的图片,视口外的其它图片先不加载,这样就可以大大减少带宽、流量。加快渲染速度,提高用户体验
可使用 IntersectionObserver api
实现图片懒加载,但只有主流浏览器支持,还是可以接受的。这里有阮老师的具体讲解
如果需要兼容一些老的浏览器,可使用 lazyestload,它是一个小而精巧的开源库,推荐使用
优化之 字体
目前(2019-08
)网络上使用的字体格式主要有:EOT、 TTF/OTF、 WOFF
和 WOFF2
WOFF
比较广泛的支持(推荐使用)WOFF2
只有现代浏览器支持(推荐使用)EOT
只有 IE 支持(IE9 以下)TTF/OTF
部分 IE 、主流浏览器支持SVG
字体兼容性很差,现在 Chrome 也放弃了对其支持,可忽略
预加载网页字体资源
js
<head>
<link rel="preload" href="fonts/awesome.woff2" as="font">
</head>
如上代码,可预加载网页字体资源。<link rel="preload">
用于"提示"浏览器尽快加载指定资源,但不会告知浏览器如何使用该资源。需要与 @font-face
配合使用,才能让浏览器知道如何解析处理给定的字体资源。
js
@font-face {
font-family:'Awesome Font';
font-style: normal;
font-weight: 400;
src: local('Awesome Font'),
url('fonts/awesome.woff2') format('woff2'),
url('fonts/awesome.woff') format('woff'),
url('fonts/awesome.ttf') format('truetype'),
url('fonts/awesome.eot') format('embedded-opentype');
}
如上代码,在 src
中优先列出 local('Your Font Name')
可确保不会针对已安装的字体发出多余的 HTTP
请求
压缩字体
因为默认情况下不会对字体进行压缩处理,所以在部署生产之前,请确保字体已经过 gzip
压缩处理。
缓存字体
字体资源比较稳定,非常适合使用缓存策略来减少带宽,优化体验
优化之 视频
移除不必要的视频
如一个响应式网站,在移动端和 PC 端都可以很好的展示页面。那就要考虑真的需要在移动端加载视频吗?!如若不需要可以使用如下方式解决:
js
/* 视口小于 650px 时,不加载对应 class 选择器的视频 */
@media screen and (max-width: 650px) {
.video_ele {
display: none;
}
}
压缩视频
可以使用 FFmpeg 工具对视频进行压缩、转换等处理。如果播放的视频不需要声音,可以去掉音轨信息减少文件体积。
按需加载
与图片一样,视频也可以按需加载,即在可视区域内时,再加载对应视频资源
js
<!-- poster 占位符,视频播放之前展示 -->
<video autoplay lazy poster="poster-image.jpg">
<source data-src="awesome.webm" type="video/webm">
<source data-src="awesome.mp4" type="video/mp4">
</video>
js
document.addEventListener('DOMContentLoaded', function () {
var lazyVideos = [].slice.call(document.querySelectorAll('video.lazy'))
if ('IntersectionObserver'inwindow) {
var lazyVideoObserver = new IntersectionObserver(function (entries, observer) {
entries.forEach(function (video) {
if (video.isIntersecting) {
for (var source in video.target.children) {
var videoSource = video.target.children[source]
if (typeof videoSource.tagName === 'string' && videoSource.tagName === 'SOURCE') {
videoSource.src = videoSource.dataset.src
}
}
video.target.load()
video.target.classList.remove('lazy')
lazyVideoObserver.unobserve(video.target)
}
})
})
lazyVideos.forEach(function(lazyVideo) {
lazyVideoObserver.observe(lazyVideo)
})
}
})
如上代码,用 IntersectionObserver api 简单实现了视频按需加载功能。跟图片懒加载类似,将可视区域的 <video>
元素的 data-src
属性更改为 src
属性。然后再调用 load
方法即可触发视频加载,当然需要有 autoplay
属性的支持。