【笔记】前端性能优化有哪些?

303 阅读10分钟

目前处在的公司对网站的性能提出了强制性的优化需求,所以我最近也接触学习或者使用了一些性能优化的方案。

性能优化的指标(RAIL)

可能大家都随时听到公司或者上级要求性能做优化,更多的就是比较笼统的说希望页面加载快一点。那么性能优化到底哪些具体的指标,从哪里下手?那么,Google Chrome团队就提出了RAIL这个性能模型。RAIL模型的理念是“以用户为中心”,这句话是什么意思呢?意思就是并不是需要你的网站在任何设备上都运行得非常的快,而是需要用户觉得满意。

RAIL分别是什么意思?

R(response):响应。这里的response并不是我们请求接口响应的意思,而是用户在网站上交互后的网站处理用户交互事件给出反馈。这个延时标准制定的是50ms以内,这个50ms这个具体的数字是怎么得来的?怎么说呢,其实这个也不算是完全一毫秒不差的一个数字,它的由来相对比较接地气。那就是谷歌有很大的一个用户群体,他会向这些用户发“调查问卷”去问用户大概能接受多久的延迟,拿到用户反馈的最大能接受100ms的延时,然后就得到的这个数字,这也完全遵循了“以用户为中心”这个理念。眼尖的同学已经看到了用户反馈的是100ms,而标准制定的是却50ms,这是为什么呢?请看下图:

image.png

A(animation):动画。现在网站为了吸引用户或者说让用户感到舒适,也就出现了越来越多的动画效果。这个指标给出的是标准说每一帧不超过16ms。

I(idle):空闲。RAIL模型要求要给主线程留有足够空闲。这个也与response相呼应,主线程留有足够的空闲时间,才能处理用户交互。想必大家也遇到过,特别是以前看网站点一下很久都没反应,这就说明当前主线程都已经忙得要死了。

L(load):加载。没错,这里就是资源网络加载的意思,当然资源加载得越快越好!

常用的性能检测方法

network:

image.png

注:上面蓝色勾选的三个选项框需勾上。

图片中数字的意思分别为:1 - 发起了多少个请求;2 - 资源大小; 3 - DOM完成加载的时间; 4 - 所有资源加载的时间。

瀑布图(waterfull):

image.png

瀑布图就把当前页面所有字段加载纵向列了出来。这个瀑布图怎么看,可以横着看,也可以竖着看。横着看就是看具体的某一个资源,竖着看就可以看到两根竖线,蓝色的是DOM加载完成,红色的是所有资源加载完成。下面我们查看一个具体的资源:

image.png

从上图中我们可以看到,content Download也就是资源下载其实是在最后一个步骤,它前面还有很多步骤。这些步骤分别是什么?

Queueing: 排队。为什么会排队呢?因为浏览器会对资源做一个优先级排序,会把高优先级的资源先安排去请求。

Cnnection start: TCP链接。

Request sent: 发送请求。

Waiting: 发送请求到请求回来的等待时间。这个是用户最为直观感受的一个等待。这个指数高说明用户等待的就会比较久。影响这个指标的因素大概有:后台处理能力,网络好坏。

lighthouse:

image.png

上图表明该网站性能得分为65分(满分100分)。当然,每次测量的时候网络状况可能不同,所以我们可以多测几次取其平均值。下面列几个图上较为常关注的指标:

First Contentful Paint: 从白屏到第一个内容显示出来的时间。

image.png 从这里的截图中也可以看到经历了两张白屏图片。

Speed Index: 这个就是一个标准,目前为4s,什么意思呢?就是说你这个这个指标只要小于4s,那么就算快的。

Largest Contentful Paint: 用于监控网页可视区内“绘制面积”最大的元素开始呈现在屏幕上的时间点.

Time To Interactive: 用户可以第一次开始交互的时间。

performance:

image.png

前面两个检测方法可以说都是检测资源加载的性能,而performance则是资源加载后的性能检测。它能检测到页面加载后所有的操作带来的影响,当然,我们也可以具体的看到我们的操作或者说我们写的代码给页面带来了哪些影响。我们截图举个例:

image.png

image.png

image.png

image.png

image.png

上面这几张可图就是一个任务中浏览器做了哪些操作耗费了多少时间。这几张图其实就是一个重排的过程,从解析HTML,重新计算样式,布局,绘制再到复合。我们也肉眼可见在这几个步骤中,layout布局跟paint绘制花费了特别多的时间。所以有了这些数据,我们性能优化也就有了着手点。

还有很多插件也可以拿到性能指数!

说完了大致的性能检测的方法,我们也拿到了我们所有希望得到的性能指标,那么我们就开始着手进行优化吧,我把优化氛围一下几个层面依次介绍:渲染层面,代码层面,构建打包,传输层面。后续我会逐一补全各个层面常用的优化方案。

渲染优化:

说到渲染优化,首先就得清楚浏览器的渲染流程,这样你才能知道在渲染层面从哪下手去优化。这里就不细讲浏览器具体到每一步渲染流程,想了解的同学可以看看这篇文章

那么我们呢就只关注浏览器的关键渲染路径:

image.png

这个关键路径在浏览器渲染中至少会走完一次,也就是说浏览器里面至少会重排一次。

上图中差不多就展现了浏览器渲染的大致步骤,也可以通过上图顺便理解一下重排和重绘,重排的意思就是浏览器又重新执行了layout及以后的步骤,重绘的意思就是浏览器又重新执行了paint以后的步骤。在前面讲performance的时候那几张截图也是浏览器的绘制步骤,从那几张图里面可以看到这个流程中layout和paint是最消耗时间的。那么我们怎么优化呢?

哪些操作会引起重排呢?

  • 增删元素
  • display: none
  • 移动元素位置
  • 修改浏览器大小,字体大小
  • 等等

其实我们经常会听到关于性能优化就有这么一个说法尽量减少重排重绘,特别是重排。也就是说在用户做操作后尽量让浏览器不走layout和paint,而是直接走composite复合流程,这样就节省了大量的时间。答案肯定是有的,下面我们结合实例来看看怎样避免重排重绘:

image.png

我们先就以一段很简单的代码让页面上显示一行框高均为100px的红框,onload后把改变红框的大小。

第一种方法

div.style.width = '200px';div.style.height = '200px'

image.png

从上图中可以看到,因为修改了元素的宽高影响到了样式中的几何信息,这样就会触发layout布局以及后续流程。

第二种方法

div.style.transform = 'scale(2)' image.png

从上图就可以看到,这次渲染直接进入了复合流程,就避开了layout布局和paint绘制流程。css还有个属性opacity也同理。当然,在真实的项目开发中难免会有导致重排重绘的时候,所以我们只能优化则优化。

代码优化:

代码优化无非就是从我们编码过程中去考虑做优化,那么我们也从3个层面来讲优化:html,css,js。

html优化

html优化相较于其他可能较少了现在。

  • 减少iframe使用

想必现在应该很少人再用到iframe了,想之前我们公司用iframe满足微前端。。。 为什么iframe会影响性能呢?首先它自己会有一个加载过程,并且它会阻碍上一级的加载,就好比你一个页面嵌入了iframe,只要它还没加载完,你这个页面的onload就执行不了。 如果有同学实在要使用iframe的话其实也有相对而言的优化方案,那就是在你的页面onload后再去给iframe赋值src。

  • 去除空白符以及无用的注释代码

空白符以及注释代码基本上都是在开发阶段会有,在打包上线前还是去除较好,毕竟空间这个东西还是得靠挤嘛。

  • 节点避免过深层次嵌套

有很多同学不管写什么都喜欢外面用一个div包裹,其实这样没什么太大意义的。并且你的层级嵌套越深,再解析DOM树的时候消耗的性能也会比较大。就那遍历来说,你嵌套的越多肯定也就会消耗更多的时间。

css优化

css优化也可以有标准可依,就performance上面在渲染过程中你会看到Recalculate Style这个模块。这个就是我们需要关注的时间点。

  • contain

css中有个属性也可以避免父容器重排,那就是contain。这个属性是什么意思呢?它就好比我们以前经常用到的iframe,你在某个样式中设置了contain,就代表着这个页面中的这个元素与其他元素隔离,你元素内部的变动不会影响到外面其他元素。

  • will-change

will-change会经常使用到电商网站的商品列表页的每一个商品模块上面去,这种模块上面会有比较多的交互效果,比如鼠标移入图片大小变化等等,像这种样式交互特别多的就可以设置will-change,它的目的是告诉浏览器将当前这个节点独立出一个图层,以至于不影响其他图层。当然,在使用will-change的时候得节制,不能任何元素都乱用,这样不仅起不到优化的效果,甚至会影响到性能,你想一想,一来多少个图层,那么浏览器的压力就会变大。

js优化

  • v8引擎

大家都应该听说过v8引擎,它是chorme浏览器和nodejs的js引擎,它的大致解析流程是拿到js脚本进行编译,会编译成抽象语法树,也就是所说的AST,然后存储在数据结构中,然后解释器再去理解你的js代码是什么意义,然后会转为机器码去运行,在这个过程中间,v8引擎也会有一个自己的优化流程,通过Optiising Compiler编译器它会去对代码做优化,但是呢,它做的优化又不一定一直适用于我们的代码,所以它又会去做一个反优化。而这个过程,也会对性能有一定影响。下面我们用代码演示:

const { PerformanceObserver, performance } = require('perf_hooks');

const obs = new PerformanceObserver((items) => {
    console.log(items.getEntries()[0].duration);
    performance.clearMarks();
});
obs.observe({ entryTypes: ['measure'] });
const num1 = 1
const num2 = 2
const add = (a, b) => {
    return a + b
}
performance.mark('A');

for(let i = 0; i < 100000000; i++) {
    add(num1, num2)
}
for(let i = 0; i < 100000000; i++) {
    add(num1, num2)
}
performance.mark('B');

performance.measure('A to B', 'A', 'B');

上面我们写了两个for循环,然后用nodejs的性能检测工具来获取两个循环执行完毕花费了多少时间。

image.png 我们可以看到时间是140ms,那么我们修改一下代码再来看看结果。

const { PerformanceObserver, performance } = require('perf_hooks');

const obs = new PerformanceObserver((items) => {
    console.log(items.getEntries()[0].duration);
    performance.clearMarks();
});
obs.observe({ entryTypes: ['measure'] });
const num1 = 1
const num2 = 2
const add = (a, b) => {
    return a + b
}
performance.mark('A');

for(let i = 0; i < 100000000; i++) {
    add(num1, num2)
}
add(num1, 'hahaha')
for(let i = 0; i < 100000000; i++) {
    add(num1, num2)
}
performance.mark('B');

performance.measure('A to B', 'A', 'B');

image.png 我们可以看到,耗费的时间变成了512ms。这也就是触发了v8的反优化,为什么呢?因为前面循环add,参数一致或者说数据类型一致,那么v8引擎就衍生了自己的一个优化方案可一直适用,然后中间自行了add方法把其中的一个参数数据类型改为了字符串,它突然发现它现有的优化方案不适用了,那么得推翻重来,这也就引起了反优化。

  • 对象

我们都知道js是一门弱类型语言,它不像JAVA。但是v8引擎在编译的时候还是得知道它具体的类型,那它怎么知道呢?它内置有很多类型,称之为隐藏类型,它会推断出相应类型。我们先上段代码:

class Car {
    constructor(name, color) {
        this.name = name
        this.color = color
    }
}
const car1 = new Car('bc', 'red')
const car2 = new Car('ad', 'black')
const car1 = {name: 'bc'}
car1.color = 'red'
const car2 = {color: 'black'}
car2.name = 'ad'

上面两段代码中很明显第一段代码性能更高,为什么呢?v8在解析的时候当你声明了一个Car的类它有设一个隐藏类型,初始化name和color的时候也会设置,当你用类构建新的实例的时候你的执行顺序一样,它默认为复用了上一次的隐藏类型。而下面这个相当于每一步都独立设置了隐藏类型。

  • 类数组

js中尽量不要去直接遍历类数组,比如function中的arguments。而是推荐先将类数组转为数组再进行遍历。实验证明,将类数组转为数组然后再遍历性能也会比直接遍历类数组要好。

  • 数组越界
const arr = [1,2,3]
for(let i = 0; i <= arr.length; i++) {
    if (arr[i] < 10) {
        console.log('')
    }
}

从上段代码中可以看到,我们下标上从0开始的,然后我们i执行到3的时候它的函数体里的内容仍然会执行,但是arr[3]其实是一个undefined,然后有一个比较条件就成为了undefined < 10,那么我们知道,数组的原型也是Object,原型链查找变量就是一层一层往上找。所以它就会往原型链上面额外的查找。当然,这个也会报错。

构建打包优化:

这里就用相对使用率更高的webpack。

  • url-loader,file-loader

拿图片举例,file-loader会默认将图片打包到bundle.js同级。显而易见,页面要渲染这张图片就会单独发送一次http请求。url-loader则会将图片转译为base64格式放在bundle.js里面,这里省去了独立的请求但是bundle.js的体积相对而言就更大了,加载的时间也会相对更长。所以在打包的时候就得去衡量一下什么时候用file-loader什么时候用url-loader。比如图片很小,就使用url-loader,这样不需要为了一张很小的图片去发起一次http请求,并且也不会影响到加载时常。而大图片就用file-loader,避免影响主文件加载。

file-loader:

image.png

url-loader:

image.png

image.png

  • mini-css-extract-plugin

我又看到很多同事在配置产线环境的webpack的css loader的时候用的style-loader,这个肯定能解析没问题,但是它是将样式解析到快html行内,很明显,这不是一个好的体验,所以,使用mini-css-extract-plugin代替style-loader。它会将css独立解析到一个文件中。然后可以配合terser-webpack-plugin,optimize-css-assets-webpack-plugin去做css的压缩。

image.png

image.png

  • tree-shaking tree-shaking简单翻译过来可以是摇树的意思,就是将树上面枯掉没有用的叶子摇下来,这个解释可以说是很形象。tree-shaking在webpack中的左右就是打包时剔除没有用到的代码。但是需要注意的就是tree-shaking只支持es module方式导入导出的文件。说到这,就应该注意到使用webpack打包构建时,针对js文件我们会用到babel-loader,@babel/preset-env。这就是将es6的语法转为了es5的语法,这样的话tree-shaking也就会受影响,所以需要在@babel/preset-env的配置中将module改为false。

需要注意的是,以css文件举例,在文件中import一个css文件,css文件其实并没有像js文件一样通过export或者export default导出。这个时候,tree-shaking就会自动剔除掉这个css文件从而导致问题。所以我们希望某些文件不被tree-shaking掉,就需要在package.json文件中添加一个sideEffects: ['*.css']

传输加载优化: