main
# 前端性能优化
**Repository Path**: lyc458216/front-end-Performance-Optimization
## Basic Information
- **Project Name**: 前端性能优化
- **Description**: 前端性能优化 掌握行业实用专业前沿的解决方案
- **Primary Language**: JavaScript
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 2
- **Forks**: 4
- **Created**: 2020-10-06
- **Last Updated**: 2024-09-14
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# 前端性能优化
## 介绍
前端性能优化 掌握行业实用专业前沿的解决方案
## 性能优化的指标和工具
### Google Chrome network 建议配置

### 总体资源概况

对网站性能影响比较大的指标:TTFB(一个资源从请求发出去到请求回来的时间)
### 保存性能分析结果

### 查看页面帧数
```bash
shift + ctrl + p #调出控制台
# 输入框输入 frame
# 选择 FPS 选项
```

### 性能优化
#### 加载
1.speed index < 4s
2.TTFB 尽可能小
3.页面加载时间
4.首次渲染
#### 响应
交互动作的反馈时间
帧率 FPS
异步请求的完成时间
### 可量化测量模型 RAIL
Response 响应
处理事件应在 50 ms 以内完成
Animation 动画
每 10ms 产生一帧
Idle 空闲
尽可能增加空闲时间
Load 加载
在 5s 内完成内容加载并可以交互
### 性能测量工具
Chrome DevTools 开发调试、性能测评
Lighthouse 网站整体质量评估
WebPageTest 多测试地点、全面性能报告
https://webpagetest.org/
#### WebPageTest 本地部署(使用需翻墙)
docker 拉取镜像
1.拉取 server 镜像
```bash
docker pull webpagetest/server
```
2.拉取 agent 镜像
```bash
docker pull webpagetest/agent
```
3.启动 webpagetest/server 镜像
```bash
docker run -d -p 4000:80 webpagetest/server
```
4.启动 webpagetest/agent 镜像
```bash
docker run -d -p 4001:80 --network="host" -e "SERVER_URL=http://localhost:4000/work/" -e "LOCATION=Test" webpagetest/agent
```
### 创建自定义镜像方便以后自定义部署和安装
一、自定义创建 webpagetest/server 镜像
1)创建自定义目录
2)该目录下创建名为 Dockerfile 的文件,添加如下配置:
```bash
FROM webpagetest/server
ADD locations.ini /var/www/html/settings/
```
3)创建 locations.ini 文件,添加如下配置
```bash
[locations]
1=Test_loc
[Test_loc]
1=Test
label=Test Loction
group=Desktop
[Test]
browser=Chrome,Firefox
label="Test Location"
connectivity=LAN
```
4)打包该文件夹
```bash
docker build -t wpt-windows-server .
```
二、自定义创建 webpagetest/agent 镜像
1)创建自定义目录
2)该目录下创建名为 Dockerfile 的文件,添加如下配置:
```bash
FROM webpagetest/agent
ADD script.sh /
ENTRYPOINT /script.sh
```
3)创建 script.sh 文件,添加如下配置
```bash
#!/bin/bash
set -e
if [ -z "$SERVER_URL" ]; then
echo >&2 'SERVER_URL not set'
exit 1
fi
if [ -z "$LOCATION" ]; then
echo >&2 '$LOCATION not set'
exit 1
fi
EXTRA_ARGS=""
if [ -n "$NAME" ]; then
EXTRA_ARGS="$EXTRA_ARGS --name $NAME"
fi
python /wptagent/wptagent.py --server $SERVER_URL --location $LOCATION $EXTRA_ARGS --xvfb --dockerized -vvvvv --shaper none
```
4)打包该文件夹
```bash
docker build -t wpt-windows-agent .
```
运行新的镜像
```bash
docker run -d -p 4000:80 wpt-windows-server
```
```bash
docker run -d -p 4001:80 --network="host" -e "SERVER_URL=http://localhost:4000/work/" -e "LOCATION=Test" wpt-windows-agent
```
#### Lighthouse 使用
1、安装
```bash
cnpm install lighthouse -g
```
2、使用并测试
```bash
lighthouse https://www.bilibili.com
```
3、生成报告地址:
```bash
Printer html output written to C:\Users\lyc45\m.bilibili.com_2020-10-07_14-52-21.report.html +75ms
```
4、查看报告
直接在浏览器地址栏输入
C:\Users\lyc45\m.bilibili.com_2020-10-07_14-52-21.report.html 即可
#### 对资源进行手动阻止加载
chrome shift + ctrl + p
show request blocking
点击 Enable request blocking 后面的 "+"
输入
```bash
log*.js:
```
表示把所有匹配的 js 文件阻止加载
#### node 后台性能优化
使用 compression 对资源进行压缩
```js
const express = require("express");
const app = express();
//...
const compression = require("compression");
app.use(compression());
//...
```
#### 使用 chrome 的 performance 进行分析
主线程分析

在 chrome dev tools 中按 esc 键可进入常用功能菜单
建议常用功能:

使用 Chrome DevTools 进行性能测试
Audit(Lighthouse)
Throttling 调整网络吞吐
Performance 性能分析
Network 网络加载分析
#### 常用的性能测量 APIs
关键时间节点(Navigation Timing, Resource Timing)
网络状态(Network APIs)
客户端服务端协商(HTTP Client Hints)& 网页显示状态(UI APIs)
重要的时间点如何计算:
DNS 解析耗时: domainLookupEnd - domainLookupStart
TCP 连接耗时: connectEnd - connectStart
SSL 安全连接耗时: connectEnd - secureConnectionStart
网络请求耗时 (TTFB): responseStart - requestStart
数据传输耗时: responseEnd - responseStart
DOM 解析耗时: domInteractive - responseEnd
资源加载耗时: loadEventStart - domContentLoadedEventEnd
First Byte 时间: responseStart - domainLookupStart
白屏时间: responseEnd - fetchStart
首次可交互时间: domInteractive - fetchStart
DOM Ready 时间: domContentLoadEventEnd - fetchStart
页面完全加载时间: loadEventStart - fetchStart
http 头部大小: transferSize - encodedBodySize
重定向次数:performance.navigation.redirectCount
重定向耗时: redirectEnd - redirectStart
前端可以把这些重要的时间节点计算后发送给后台
```js
// load 事件后触发
window.addEventListener("load", (event) => {
// Time to Interacrtive 可交互时间
let timing = performance.getEntriesByType("navigation")[0];
// 计算 tti = timing.domInteractive - timing.fetchStart;
let tti = timing.domInteractive - timing.fetchStart;
console.log("TTI: " + tti);
});
```
前端获取长任务信息
```js
// 通过 PerformanceObserver 得到所有的 long tasks 对象
let observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log(entry);
}
});
// 监听long tasks
observer.observe({ entryTypes: ["longtask"] });
```
了解客户还是不是在看当前页面
```js
let vEvent = "visibilitychange";
// webkit 浏览器
if (document.webkitHidden != undefined) {
// webkit 事件名称
vEvent = "webkitvisibilitychange";
}
function visibilitychanged() {
if (document.hidden || document.webkitHidden) {
// 页面不可见
console.log("Web page is hidden");
} else {
// 页面可见
console.log("Web page is visibile");
}
}
document.addEventListener(vEvent, visibilitychanged, false);
```
获取客户端网络状态
```js
let connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
// 网络连接状态
let type = connection.effectiveType;
// 网络状态发生变化触发的函数
function updateConnectionStatus(){
console.log("connection type change from " + type + "to " + connection.effectiveType);
}
// 添加网络监听,一旦网络状态发生变化就触发 updateConnectionStatus 方法
connection.addEventListener('change', updateConnectionStatus)
```
## 渲染优化
### 浏览器的渲染流程

### 回流
对于浏览器的二次绘制成为回流(重绘)
会导致重绘的操作:

#### 查看页面中是否有回流
页面上代码:
```js
// 获取页面上的卡片
let cards = document.getElementsByClassName("MuiCardMedia-root");
const update = () => {
cards[0].style.width = "800px";
};
```
浏览器中识别有没有回流发生


#### 避免回流
元素的位置属性的读(强制对布局进行重新计算)与写都会导致回流,连续不断的强制回流会导致布局抖动。
```js
// 布局抖动示例
// 获取所有页面上的卡片元素
let cards = document.getElementsByClassName("MuiCardMedia-root");
// 一个连续触发的更新卡片图片宽度的方法
const update = (timestamp) => {
for (let i = 0; i < cards.length; i++) {
// 获取 offsetTop,设置新的 width
cards[i].style.width =
(Math.sin(cards[i].offsetTop + timestamp / 1000) + 1) * 500 + "px";
}
window.requestAnimationFrame(update);
};
window.addEventListener("load", update);
```
避免回流一个比较重要的方法就是在需要改变元素位置的时候不要使用 left,right,top,bottom 这样的属性;
而是使用 transform translate 这样的属性,这种属性不会导致回流,只是会影响复合的过程。
第二个就是使用虚拟 dom 与原 dom 进行比对,然后做出修改,把批量的修改进行统一处理,避免多次回流。
第三个是读写分离:offset(读) 与设置 css 位置属性(写)分开。进行批量的读和批量的写分开。
#### FastDom: 布局抖动解决方案
FastDom 两个比较重要的 API: fastdom.measure(读操作)、fastdom.mutate(写操作)。
对上面的代码进行修改:
```js
// 使用 fastdom 防止布局抖动
// ...
for (let i = 0; i < cards.length; i++) {
fastdom.measure(() => {
// 读取 offsetTop 值
let top = cards[i].offsetTop;
});
// 写
fastdom.mutate(() => {
// 获取 offsetTop,设置新的 width
cards[i].style.width = (Math.sin(top + timestamp / 1000) + 1) * 500 + "px";
});
}
// ...
```
#### 复合线程(compositor thread)与图层(layers)
查看页面图层:
chrome devtools: shift + ctrl + p -> 搜索 layers ->(show layers)
图层页面:

#### 只触发复合而不触发重绘和重排
以下 4 个属性只触发复合
```css
{
/* 修改位置 */
Position transform: translste(npx, npx);
/* 修改大小 */
Scale transform: scale(n);
Rotation transform: rotate(ndeg);
Opacity opacity: 0...1;
}
```
复合线程做了什么
将页面拆分图层进行绘制再进行复合
利用 devTools 了解网页的图层拆分情况
哪些样式仅影响复合
使用 translate 属性避免重排和重绘
```css
{
keyframes rotate {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
}
```
性能分析查看:

#### 使用浏览器捕获重绘区域
shift + ctrl + p 搜索 Rendering 选择 show Rendering

在浏览器上使用 will-change 属性告知浏览器该元素会有哪些变化的方法,这样浏览器可以在元素属性真正发生变化之前提前做好对应的优化准备工作。 这种优化可以将一部分复杂的计算工作提前准备好,使页面的反应更为快速灵敏。
用好这个属性并不是很容易:
不要将 will-change 应用到太多元素上:浏览器已经尽力尝试去优化一切可以优化的东西了。有一些更强力的优化,如果与 will-change 结合在一起的话,有可能会消耗很多机器资源,如果过度使用的话,可能导致页面响应缓慢或者消耗非常多的资源。
有节制地使用:通常,当元素恢复到初始状态时,浏览器会丢弃掉之前做的优化工作。但是如果直接在样式表中显式声明了 will-change 属性,则表示目标元素可能会经常变化,浏览器会将优化工作保存得比之前更久。所以最佳实践是当元素变化之前和之后通过脚本来切换 will-change 的值。
不要过早应用 will-change 优化:如果你的页面在性能方面没什么问题,则不要添加 will-change 属性来榨取一丁点的速度。 will-change 的设计初衷是作为最后的优化手段,用来尝试解决现有的性能问题。它不应该被用来预防性能问题。过度使用 will-change 会导致大量的内存占用,并会导致更复杂的渲染过程,因为浏览器会试图准备可能存在的变化过程。这会导致更严重的性能问题。
给它足够的工作时间:这个属性是用来让页面开发者告知浏览器哪些属性可能会变化的。然后浏览器可以选择在变化发生前提前去做一些优化工作。所以给浏览器一点时间去真正做这些优化工作是非常重要的。使用时需要尝试去找到一些方法提前一定时间获知元素可能发生的变化,然后为它加上 will-change 属性。
```css
root {
will-change: "transform";
}
```
添加该属性可以让浏览器把可能会发生变化的 transform 元素抽离到单独图层。
#### 减少重绘的方案
利用 Devtools 识别 paint 的瓶颈
利用 will-change 创建新的图层
#### 高频 事件处理函数 防抖

#### 使用 rAF 函数优化高频事件处理函数
未使用 rAF 优化处理高频事件
```js
// 侦听鼠标移动事件
// 利用 chrome devtools 可以复现抖动的问题(pointer Events)
changeWidth(pos){
//...
}
window.addEventListener('pointermove', (e)=>{
let pos = e.clientX;
changeWidth(pos)
})
```
使用 rAF 做优化:
```js
let ticking = false;
window.addEventListener("pointermove", (e) => {
let pos = e.clientX;
if (ticking) return;
ticking = true;
window.requestAnimationFrame(() => {
changeWidth(pos);
ticking = false;
});
});
```
#### React 时间调度实现
requestIdleCallback:如果一帧(16ms)还有空余时间,可以做其他的事情的回调函数,但不同浏览器的实现不好(兼容性不好)。
react 内部通过 rAF 模拟了 rIC

#### JS 的开销和如何缩短解析时间
同样大小的 JS 文件和 JPEG 文件开销对比:

解决方案:
Code splitting 代码拆分。按需加载
Tree shaking 代码减重
减少主线程工作量:
避免长任务
避免超过 1kb 的行间脚本(因为浏览器引擎在处理行间脚本时无法做出合理的优化)
使用 rAF 和 rIc 进行时间调度

#### V8 编译原理

当 v8 把代码优化后如果发现优化的不对还会进行返优化(撤销已有的优化)这样会导致性能下降,举例:
保存文件,在 Node 环境下运行
```js
const { performance, PerformanceObserver } = require("perf_hooks");
const add = (a, b) => a + b;
const num1 = 1;
const num2 = 2;
performance.mark("start");
for (let i = 0; i < 10000000; i++) {
add(num1, num2);
}
add(num1, "s"); // 因为上面的 add(num1, num2) 和下面的 add(num1, num2) 执行结果是一样的,v8 引擎会自动对代码进行优化,但这段代码 add(num1, "s") 出现了字符串 's' 会导致浏览器自动优化有误,而进行反优化,导致性能下降。
for (let i = 0; i < 10000000; i++) {
add(num1, num2);
}
performance.mark("end");
const observer = new PerformanceObserver((list) => {
console.log(list.getEntries()[0]);
});
observer.observe({ entryTypes: ["measure"] });
performance.measure("测量1", "start", "end");
```
抽象语法树:
源码 => 抽象语法树 => 字节码 Bytecode => 机器码
编译过程会进行优化
运行时可能发生反优化
#### V8 优化机制
脚本流:当下载的脚本大于 30kb 时会单独开一个线程,给这段代码先做解析,然后边下载边解析,最后把所有解析的结果进行合并。
字节码缓存:源码翻译成字节码之后把可能会重复用到的代码进行缓存。
懒解析:对函数的声明不一定会直接用,所以遇到函数时先不解析,等到用的时候再解析。
有的时候不希望浏览器进行懒解析,希望立即解析,如何做:
```js
// 懒解析
let add = (a, b) => a + b;
// 饥饿加载(立即加载)
// let add = ((a, b) => a + b);
```
当使用 webpack4 以下的版本打包时会自动把 let add = ((a, b) => a + b); 两边的括号去掉,需要使用 Optimize.js 优化初次加载时间。
#### 对象优化可以做哪些
1、以相同顺序初始化对象成员,避免隐藏类的调整;
2、实例化后避免添加新属性;
3、尽量使用 Array 代替 array-like;
```js
// 类数组想调用 forEach 方法:
Array.prototype.forEach.call(arrObj, (value, index) => {
// 不如在真实数组上效率高
console.log(`${index}:${value}`);
});
// 优化:
const arr = Array.prototype.slice.call(arrObj, 0);
arr.forEach((value, index) => {
console.log(`${index}:${value}`);
});
```
4、避免读取超过数组的长度;
5、避免元素类型转换。
#### HTML 优化
1、减小 iframes s 使用;
2、压缩空白符;
3、避免节点深层级嵌套;
4、删除元素默认属性;
5、避免 table 布局;
6、删除注释;
7、css & js 尽量外链.
#### CSS 性能优化
1、减低 CSS 对渲染的阻塞:把 css 文件进行拆分,先加载首次需要用到的,对暂时不需要的放到后面进行加载;
2、利用 GPU 对动画进行完成:多图层处理;
3、使用 contain 属性:
```css
{
/* 告诉浏览器盒子内的变化不会影响到盒子外面,盒子外的变化也不会影响到盒子里面 */
contain: layout;
}
```
4、使用 font-display 属性:让文字更早的显示到页面上。
### 资源优化
减少 http 请求数量
减少请求资源的大小
#### 图片资源优化
jpg/jpeg 特点:压缩率高,色彩丰富,适合展示物品,不适合对边缘质量要求高的图片。
图片压缩工具:
github.com/imagemin/imagemin
png 特点:对 jpg/jpeg 的缺点进行了弥补,但是体积较大。
压缩工具:
github.com/imagemin/imagemin-pngquant
webP 格式
#### 图片的懒加载
使用 img 原生的懒加载
```html
```
第三方图片懒加载方案:
verlok/lazyload
yall.js
Blazy
react 插件地址:
github.com/Aljullu/react-lazy-load-image-component
基本配置:
```html
```
#### 字体优化
使用 font-display

使用方法:
```css
@font-face {
/* ... */
font-display: block;
}
```
### 构建优化
webpack 默认配置:
https://webpack.js.org/configuration/mode/#mode-production
#### Tree-shaking
上下文未用到的代码(dead code)
基于 ES6 import export
package.json 中配置 sideEffects,处理可能会影响全局而无法通过 export 体现的文件
注意 Babel 默认配置的影响
```js
{
presets: [
[
"@babel/preset-env",
{
// preset-env 会把 es6 的模块化语法改为其他的模块化语法,但 tree-shaking 是基于 es6 的模块化语法的,所以要改为 false,告诉 babel 不要转换
modules: false,
targets:{
// 对市场份额超过 0.25% 的浏览器都做兼容
browsers: ['>0.25%']
},
// 对不需要的 polyfill 不打包处理
useBuiltIns: "usage",
},
],
],
plugins:[
// 配置辅助函数的按需引入
'@babel/plugin-transform-runtime'
],
module:{
// 告诉 babel 不需要打包的库
noParse: /lodash/
}
}
```
#### 使用 DllPlugin 加快打包速度
避免打包时对不变的库重复构建,而是生成 dll 动态链接库,提高构建速度。
使用方法(对 react、react-dom 生成动态链接库):
webpack.dll.config.js:
```js
const path = require("path");
const webpack = require("webpack");
module.exports = {
mode: "production",
entry: {
// 设置自己希望要创建的文件名叫 'react'
// 希望要创建的动态链接库包含的类为 react 和 react-dom
react: ["react", "react-dom"],
},
outpot: {
// 输出的名字为在入口定义的名字(这里就是 react)
filename: "[name].dll.js",
// 存放路径
path: path.resolve(__dirname, "dll"),
// 库的名称为在入口定义的名字(这里就是 react)
library: "[name]",
},
plugins:[
// 通过 webpack.DllPlugin 插件生成 dll 文件的描述文件
new webpack.DllPlugin(options:{
// name 与上面的 library: "[name]" 的 name 保持一致
name: '[name]',
// 生成描述文件的路径
path: path.resolve(__dirname, 'dll/[name].manifest.json')
})
]
};
```
然后在 node 环境下运行这个脚本
然后在 webpack.config.js 中做如下配置:
```js
plugins:[
new DllReferencePlugin(options:{
manifest: require(`${__dirname}/dll/react.manifest.json)`)
})
],
```
#### webpack 代码拆分的方法
方法 1、手工定义入口,但这样要自己维护,不方便;
方法 2、使用 splitChunks 提取公有代码,拆分业务代码与第三方库;
webpack.cinfig.js 配置:
```js
{
optimization:{
splitChunks:{
// splitChunks 配置
cacheGroups:{
// 进行分组
vendor:{
// 第三方库的配置
name: 'vendor',
// 匹配的目录(因为是第三方库,所以要匹配 node_modules)
test: /[\\/]node_modules[\\/]/,
// 最小大小,默认为 30kb
minSize: 0,
// 最少拆成多少段
minChunks: 1,
// 优先级
priority: 10,
chunks: 'initial'
},
common:{
// 提取公共的代码
name: 'common',
test: /[\\/]src[\\/]/,
chunks: 'all',
minSize: 0,
minChunks: 2
}
}
}
}
}
```
#### 同步加载库与异步加载库
```js
// 同步加载
import { add } from "./math";
console.log(add(16, 26));
// 异步加载
import("./math").then((math) => {
console.log(math.add(16, 26));
});
```
#### React 中组件动态引入与 suspense
```js
const Card = lazy(factory:()=>import('./Card'))
// ...
cards.push(model.map(panel =>{
main