关于前端性能优化的知识点
“春江水暖鸭先知,产品好坏客户知”,作为前端开发,我们更注重客户体验,产品的好坏决定着客户的体验,那么一款产品的好坏有很多因素,其中性能是决定因素,那么怎么优化才能让产品的性能达到优良,让客户体验良好,今天我就带大家去了解学习前端性能优化。
优化的目的
优化的目的在于让页面加载的更快,对用户操作响应更及时,为用户带来更好的用户体验,对于开发者来说优化能够减少页面请求数,能够节省资源。
前端优化的方法有很多种,可以将其分为两大类,第一类是页面级别的优化如http请求数,内联脚本的位置优化等,第二类为代码级别的优化,例Javascript中的DOM 操作优化、CSS选择符优化、图片优化以及 HTML结构优化等等。
优化哪些?
那么我们需要优化那些点呢?
加载资源优化
渲染优化
浏览器缓存策略
图片优化
节流与防抖
加载资源优化
说起加载,当我们输入URL时,我们要知道这中间发生了什么?
首先做 DNS 查询,如果这一步做了智能 DNS 解析的话,会提供访问速度最快的 IP 地址回来
接下来是 TCP 握手,应用层会下发数据给传输层,这里 TCP 协议会指明两端的端口号,然后下发给网络层。网络层中的 IP 协议会确定 IP 地址,并且指示了数据传输中如何跳转路由器。然后包会再被封装到数据链路层的数据帧结构中,最后就是物理层面的传输了
TCP 握手结束后会进行 TLS 握手,然后就开始正式的传输数据
数据在进入服务端之前,可能还会先经过负责负载均衡的服务器,它的作用就是将请求合理的分发到多台服务器上,这时假设服务端会响应一个 HTML 文件
首先浏览器会判断状态码是什么,如果是 200 那就继续解析,如果 400 或 500 的话就会报错,如果 300 的话会进行重定向,这里会有个重定向计数器,避免过多次的重定向,超过次数也会报错
浏览器开始解析文件,如果是 gzip 格式的话会先解压一下,然后通过文件的编码格式知道该如何去解码文件
文件解码成功后会正式开始渲染流程,先会根据 HTML 构建 DOM 树,有 CSS 的话会去构建 CSSOM 树。如果遇到 script 标签的话,会判断是否存在 async 或者 defer ,前者会并行进行下载并执行 JS,后者会先下载文件,然后等待 HTML 解析完成后顺序执行,如果以上都没有,就会阻塞住渲染流程直到 JS 执行完毕。遇到文件下载的会去下载文件,这里如果使用 HTTP 2.0 协议的话会极大的提高多图的下载效率。
初始的 HTML 被完全加载和解析后会触发 DOMContentLoaded 事件
CSSOM 树和 DOM 树构建完成后会开始生成 Render 树,这一步就是确定页面元素的布局、样式等等诸多方面的东西
在生成 Render 树的过程中,浏览器就开始调用 GPU 绘制,合成图层,将内容显示在屏幕上了
我们从输入 URL 到显示页面这个过程中,涉及到网络层面的,有三个主要过程:
DNS 解析
TCP 连接
HTTP 请求/响应
这里我们就不用去管DNS解析和TCP链接了,毕竟不是我们的事,也干不来,但是HTTP请求和响应是我们优化的重点。
HTTP优化可分为两个方面:
尽量减少请求次数
尽量减少单次请求所花费的时间
减少请求数:
合理的设置http缓存,恰当的缓存设置可以大大减少http请求。要尽可能地让资源能够在缓存中待得更久。
从设计实现层面简化页面,保持页面简洁、减少资源的使用时是最直接的。
资源合并与压缩,尽可能的将外部的脚本、样式进行合并,多个合为一个。
CSS Sprites,通过合并 CSS图片,这是减少请求数的一个好办法
内联脚本的位置:
浏览器是并发请求的,很多时候我们会加入很多的外链脚本,而外链脚本在加载时却常常阻塞其他资源,例如在脚本加载完成之前,它后面的图片、样式以及其他脚本都处于阻塞状态,直到脚本加载完成后才会开始加载。如果将脚本放在比较靠前的位置,则会影响整个页面的加载速度从而影响用户体验。所以说尽可能的将脚本往后挪,减少对并发下载的影响。
渲染优化
客户端的渲染
前端去取后端的数据生成DOM树,加载过来后,自己在浏览器由上而下跑执行JS,随后就会生成相应的DOM。
优点:
客户端的渲染使得前后端分离,开发效率高
用户体验更好,我们将网站做成SPA(单页面应用)或者部分内容做成SPA,当用户点击时,不会形成频繁的跳转
缺点:
前端响应速度慢,特别是首屏,这样用户是受不了的
不利于SEO优化,因为爬虫不认识SPA,所以它只是记录了一个页面
服务端的渲染
DOM树在服务端生成,然后返回给前端,页面上展现的内容,我们在HTML源文件也能找到。
优点:
服务端渲染尽量不占用前端的资源,前端这块耗时少,速度快
利于SEO优化,因为在后端有完整的html页面,所以爬虫更容易爬取信息
缺点:
不利于前后端分离,开发的效率降低了
对html的解析,对前端来说加快了速度,但是加大了服务器的压力
类似企业级网站,主要功能是页面展示,它没有复杂的交互,并且需要良好的SEO,那我们应该使用服务端渲染。
现在很多网站使用服务端渲染和客户端渲染结合的方式:首屏使用服务端渲染,其他页面使用客户端渲染。这样可以保证首屏的加载速度,也完成了前后端分离。
区分:源码里如果能找到前端页面中的内容文字,那就是在服务端构建的DOM,就是服务端渲染,反之是客户端渲染。
浏览器渲染
浏览器渲染机制一般分为:
分析HTML并构建DOM树
分析CSS构建CSSOM树
将DOM和CSSOM合并成一个渲染树
根据渲染树布局,计算每个节点的位置
调用GPU绘制,合成图层,显示页面
在渲染DOM的时候,浏览器所做的事情:
获取DOM后分割为多个图层
对每个图层的节点计算样式结果(recalculate style -- 样式重计算)
为每个节点生成图形和位置(layout -- 回流和重布局)
将每个节点绘制填充到图层位图中(paint setup 和 paint -- 重绘)
图层作为纹理上传至GPU
复合多个图层到页面上生成最终屏幕图像(composite layers -- 图层重组)
新建独立图层会减少重回回流带来的影响,但是在图层重组的时候会消耗大量的性能,所以要权衡利弊,有所选择。
渲染流程的CSS优化
CSS的渲染是从右到左进行匹配的,我们应该注意:
避免大量使用通配符,可选择需要用到的元素
关注可以通过继承实现的属性,避免重复匹配,重复定义
少用标签选择器,例如.header ul li a
id和class选择器不应该被多余的选择器拖后腿,例如.header#title
减少嵌套,后代选择器的开销最高,不要一大串,要将选择器的深度降到最低,尽可能使用类来关联每一个标签元素。
CSS阻塞
我们将css放在head标签里和尽快启用CDN实现静态资源加载速度的优化,因为只要CSSOM不OK,那么渲染就不会完成。
JS阻塞
JS引擎是独立于渲染引擎存在的,就是说插在页面那,就在那执行,浏览器遇到script标签时,它就会停止交于JS引擎渲染,等它渲染完,浏览器又交于渲染引擎继续CSSOM和DOM的构建。
DOM渲染优化
也就是说重绘回流问题
回流:前面我们通过构造渲染树,我们将可见DOM节点以及它对应的样式结合起来,可是我们还需要计算它们在设备视口(viewport)内的确切位置和大小,这个计算的阶段就是回流。
重绘:最终,我们通过构造渲染树和回流阶段,我们知道了哪些节点是可见的,以及可见节点的样式和具体的几何信息(位置、大小),那么我们就可以将渲染树的每个节点都转换为屏幕上的实际像素,这个阶段就叫做重绘节点。
当页面布局和几何信息发生变化的时候,就需要回流。比如以下情况:
添加或删除可见的DOM元素
元素的位置发生变化
元素的尺寸发生变化(包括外边距、内边框、边框大小、高度和宽度等)
内容发生变化,比如文本变化或图片被另一个不同尺寸的图片所替代。
页面一开始渲染的时候(这肯定避免不了)
浏览器的窗口尺寸变化(因为回流是根据视口的大小来计算元素的位置和大小的)
注意:回流一定会触发重绘,而重绘不一定会回流,回流比重绘做的事情要多,带来的开销也大,在开发中,要从代码层面出发,尽可能把回流和重绘的次数最小化。
如何最小化重绘和重排
用 translate 替代 top
用 opacity 替代 visibility
不要一条一条的修改 DOM 的样式,预先定义好 class,然后修改 DOM 的 className
把 DOM 离线后修改,比如:先把 DOM 给 display: none(有一次 reflow),然后修改100次,然后再显示出来
不要把 DOM 节点的属性值放在一个循环里当成循环里的变量
不要使用 table 布局,可能很小的一个改动就会造成整个 table 的重新布局
动画实现的速度的选择
对于动画新建图层
启用 GPU 硬件加速
浏览器缓存
强缓存
发现有缓存直接用。
Expires: 绝对时间,判断客户端日期是否超过这个时间
Cache-Control:相对时间,判断访问间隔是否大于3600秒
//在设定时间之前不会和服务端进行通信了
//如果两个都下发以后者为准
协商缓存
询问服务器缓存是否可以用,在进行判断是否用。
Last-Modified/If-Modified-Since
第一次请求,respone的header加上Last-Modified(最后修改时间)
再次请求,在request的header上加上If-Modified-Since
和服务端的最后修改时间对比,如果没有变化则返回304 Not Modified,但是不会返回资源内容;如果有变化,就正常返回资源内容。浏览器收到304的响应后,就会从缓存中加载资源
如果协商缓存没有命中,浏览器直接从服务器加载资源时,Last-Modified的Header在重新加载的时候会被更新
Etag/If-None-Match
这两个值是由服务器生成的每个资源的唯一标识字符串,只要资源有变化就这个值就会改变;其判断过程与Last-Modified/If-Modified-Since类似,他可以精确到秒的更高级别。
DNS预解析
<meta http-equiv="x-dns-prefetch-control" content="on">
在一些浏览器的a标签是默认打开dns预解析的,在https协议下dns预解析是关闭的,加入mate后会打开。
图片优化
减小图片大小
我看到有的文章通过计算图片大小来优化图片,就是说:
比如一张100*100的图片,图片上有10000个像素点,如果每个像素的值是RGBA存储的话,那么也就是说每个像素有4个通道,每个通道1个字节(8位 = 1个字节),所以该图片的大小大概为39KB。所以说通过:
减少像素点
减少每个像素点能够显示的颜色
上面的两种方式减小图片的大小,不过在我们开发中直接压缩来减小图片的大小。
改变图片的格式
这个图片的类型也决定着图片的属性,详细的我再微头条说过,附上链接:
节流和防抖
日常开发过程中,滚动事件做复杂计算频繁调用回调函数很可能会造成页面的卡顿,这时候我们更希望把多次计算合并成一次,只操作一个精确点,JS把这种方式称为debounce(防抖)和throttle(节流)
函数节流
当持续触发事件时,保证在一定时间内只调用一次事件处理函数,意思就是说,假设一个用户一直触发这个函数,且每次触发小于既定值,函数节流会每隔这个时间调用一次
用一句话总结防抖和节流的区别:防抖是将多次执行变为最后一次执行,节流是将多次执行变为每隔一段时间执行
实现函数节流我们主要有两种方法:时间戳和定时器
var throttle = function(func, delay) {
var prev =Date.now();
return function() {
var context = this; //this指向window
var args = arguments;
var now =Date.now();
if (now - prev >= delay) {
func.apply(context, args);
prev =Date.now();
}
}
}
function handle() {
console.log(Math.random());
}
window.addEventListener('scroll', throttle(handle, 1000));
这个节流函数利用时间戳让第一次滚动事件执行一次回调函数,此后每隔1000ms执行一次,在小于1000ms这段时间内的滚动是不执行的
函数防抖
当持续触发事件时,一定时间段内没有再触发事件,事件处理函数才会执行一次,如果设定时间到来之前,又触发了事件,就重新开始延时。也就是说当一个用户一直触发这个函数,且每次触发函数的间隔小于既定时间,那么防抖的情况下只会执行一次。
function debounce(fn, wait) {
var timeout =null; //定义一个定时器
return function() {
if(timeout !==null)
clearTimeout(timeout); //清除这个定时器
timeout = setTimeout(fn, wait);
}
}
// 处理函数
function handle() {
console.log(Math.random());
}
// 滚动事件
window.addEventListener('scroll', debounce(handle, 1000));
如上所见,当持续触发scroll函数,handle函数只会在1秒时间内执行一次,在滚动过程中并没有持续执行,有效减少了性能的损耗。
防抖和节流能有效减少浏览器引擎的损耗,防止出现页面堵塞卡顿现象。
总结
上面我们主要从加载资源优化、渲染优化、浏览器缓存策略、图片优化、节流与防抖这几个方面,讲述了我们平常不易掌握和了解的性能优化知识点,希望大家可以了解学习掌握并加以应用,让我们的产品体验更佳,精益求精,做最好的产品和自己。