前端性能优化笔记

前端性能优化笔记

1. 如何减少首屏加载时间

1.1 减小入口文件体积

常用的手段是路由懒加载,把不同路由对应的组件分割成不同的代码块,待路由被请求的时候会单独打包路由,使得入口文件变小,加载速度大大增加。

vue-router配置路由的时候,采用动态加载路由的形式

1
2
3
4
5
6
7
8
routes:[ 
{
path: 'Blogs',
name: 'ShowBlogs',
component: () => import('./components/ShowBlogs.vue')
},
{...}
]

以函数的形式加载路由,这样就可以把各自的路由文件分别打包,只有在解析给定的路由时,才会加载路由组件。

1.2 UI框架按需加载

在日常使用UI框架,例如element-UI、或者antd,我们经常性直接引用整个UI库。

1
2
import ElementUI from 'element-ui'
Vue.use(ElementUI)

但实际上我用到的组件只有按钮,分页,表格,输入与警告 所以我们要按需引用。

1
2
3
4
import { Button, Input, Pagination, Table, TableColumn, MessageBox } from 'element-ui';
Vue.use(Button)
Vue.use(Input)
Vue.use(Pagination)

1.3 图片资源压缩

图片资源虽然不在编码过程中,但它却是对页面性能影响最大的因素。

对于所有的图片资源,我们可以进行适当的压缩。

对页面上使用到的icon,可以使用在线字体图标,或者雪碧图,将众多小图标合并到同一张图上,用以减轻http请求压力。

1.4 DNS解析优化

如果一个页面中有很多外部资源,例如JavaScript、CSS、或者图片等资源,每次解析页面的时候都需要将这些资源的域名解析成IP地址。

同步解析:

异步解析:

我们可以使用link元素,在页面前面对资源的域名进行异步解析:

1
<link rel="dns-prefetch" href="https://www.coffee.com"/>

在实际项目中可以这么做:

  1. 编写一个自动生成link标签的dns-prefetch.js文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
const fs = require('fs');
const path = require('patch');
const { parse } = require('node-html-parser');
const { glob } = require('glob');
const urlRegex = require('url-regex');

// 获取外部链接的正则表达式
const urlPattern = /(https?:\/\/[^/]*)/i;
const urls = new Set();

// 遍历dist目录中的所有HTML、js、css文件
async function searchDomain() {
const files = await glob('dist/**/*.{html,css,js}');
for (const file of files) {
// 读取文件内容
const source = fs.readFileSync(file, 'utf-8');
// 匹配链接
const matches = source.match(urlRegex({strict: true}));
if (matches) {
matches.forEach((url) => {
const match = url.match(urlPattern);
if (match && match[1]) {
// 将域名加入到set中
urls.add(match[1]);
}
});
}
}
}

// 将域名转换成link元素
async function insertLinks() {
const files = await glob('dist/**/*.html');
const links = [...urls]
.map((url) => `<link rel="dns-prefetch" href="${url}"/>`)
.join('\n');

for (const file of files) {
const html = fs.readFileSync(file, 'utf-8');
// 将html字符串解析成DOM树
const root = parse(html);
const head = root.querySelector('head');
// 在<head>标签中加入预取链接
head.insertAdjacentHTML('afterbegin', links);
fs.writeFileSync(file, root.toString());
}
}

async function main() {
await searchDomain();
await insertLinks();
}

main();
  1. 在package.json打包文件中进行配置:

![截屏2023-10-24 10.08.21](/Users/zhangwenxiao/Documents/笔记/img/截屏2023-10-24 10.08.21.png)

这样在npm run build构建项目的时候就会自动运行写好的函数,提取出外部链接构建link标签,进行域名的异步解析。

2. 重排与重绘

2.1 页面生成的过程

  1. HTML 被 HTML 解析器解析成 DOM 树
  2. CSS 被 CSS 解析器解析成 CSSOM 树
  3. 结合 DOM 树和 CSSOM 树,生成一棵渲染树(Render Tree),这一过程称为 Attachment
  4. 生成布局(flow),浏览器在屏幕上“画”出渲染树中的所有节点
  5. 将布局绘制(paint)在屏幕上,显示出整个页面

第四步和第五步是最耗时的部分,这两步合起来,就是我们通常所说的渲染

2.2 渲染

在页面的生命周期中,**网页生成的时候,至少会渲染一次。在用户访问的过程中,还会不断触发重排(reflow)和重绘(repaint)**,不管页面发生了重绘还是重排,都会影响性能,最可怕的是重排,会使我们付出高额的性能代价,所以我们应尽量避免。

2.3 重排(回流)> 重绘

单单改变元素的外观,肯定不会引起网页重新生成布局,但当浏览器完成重排之后,将会重新绘制受到此次重排影响的部分。比如改变元素高度,这个元素乃至周边dom都需要重新绘制。
也就是说:重绘不一定导致重排,但重排一定会导致重绘。

2.4 重排 (reflow)

概念

当DOM的变化影响了元素的几何信息(元素的的位置和尺寸大小),浏览器需要重新计算元素的几何属性,将其安放在界面中的正确位置,这个过程叫做重排。
重排也叫回流,简单的说就是重新生成布局,重新排列元素。

发生重排的情况

  • 页面初始渲染,这是开销最大的一次重排
  • 添加/删除可见的DOM元素
  • 改变元素位置
  • 改变元素尺寸,比如边距、填充、边框、宽度和高度等
  • 改变元素内容,比如文字数量,图片大小等
  • 改变元素字体大小
  • 改变浏览器窗口尺寸,比如resize事件发生时
  • 激活CSS伪类(例如::hover)
  • 设置 style 属性的值,因为通过设置style属性改变结点样式的话,每一次设置都会触发一次reflow
  • 查询某些属性或调用某些计算方法:offsetWidth、offsetHeight等,除此之外,当我们调用 getComputedStyle方法,或者IE里的 currentStyle 时,也会触发重排,原理是一样的,都为求一个“即时性”和“准确性”。

重排影响的范围

由于浏览器渲染界面是基于流式布局模型的,所以触发重排时会对周围DOM重新排列,影响的范围有两种:

  • 全局范围:从根节点html开始对整个渲染树进行重新布局。
  • 局部范围:对渲染树的某部分或某一个渲染对象进行重新布局

2.5 重绘 (repaints)

概念

当一个元素的外观发生改变,但没有改变布局,重新把元素外观绘制出来的过程,叫做重绘。

2.6 如何减少重排和重绘

  1. 避免设置多层内联样式
  2. 使用absolute或者fixed,使元素脱离文档流,这样他们发生变化就不会影响其他元素
  3. 避免使用table布局,一个小的改动可能会使整个table进行重新布局
  4. 将DOM的多个读操作(或者写操作)放在一起,而不是读写操作穿插着写

浏览器会将所有的回流、重绘的操作放在一个队列中,当队列中的操作到了一定的数量或者到了一定的时间间隔,浏览器就会对队列进行批处理。这样就会让多次回流、重绘变成一次回流重绘。

3. 防抖函数

防抖是指在事件被触发n秒后再执行,如果在这n秒内事件又被触发,则重新计时

3.1 非立即执行版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<div>
<button onclick="btnClick()">按钮</button>
</div>

<script>
function debounce(fn, wait) {
let timer = null;

return function() {
const context = this;
const args = arguments;

// 如果此时存在计时器,则取消之前的计时器重新计时
if(timer) {
clearTimeout(timer);
timer = null;
}

// 设置计时器,setTimeout返回值 timeoutID 是一个正整数,表示由 setTimeout() 调用创建的定时器的编号。
timer = setTimeout(() => {
fn.apply(context, args);
}, wait);
}
}

function handler() {
console.log('点击按钮');
}
const btnClick = debounce(handler, 1000);
</script>

3.2 立即执行版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<div>
<button onclick="btnClick()">按钮</button>
</div>

<script>
function debounce(fn, wait) {
let timer = null;

return function() {
const context = this;
const args = arguments;
const callNow = !timer;

if(timer) {
clearTimeout(timer);
}

timer = setTimeout(() => {
timer = null;
}, wait);

if (callNow)
fn.apply(context, args);
}
}

function handler() {
console.log('点击按钮');
}
const btnClick = debounce(handler, 1000);
</script>

4. 节流函数

节流是指规定的时间内,只能触发一次这个事件,如果在同一个规定时间内,事件被触发多次,只有一次能生效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<div>
<button onclick="btnClick()">按钮</button>
</div>

<script>
function throttle(fn, delay) {
let curTime = Date.now();

return function() {
const context = this;
const args = arguments;
const nowTime = Date.now();

if (nowTime - curTime >= delay) {
curTime = Date.now();
fn.apply(context, args);
}
}
}

function handler() {
console.log('点击按钮');
}
const btnClick = throttle(handler, 1000);
</script>
作者

zwx

发布于

2024-04-21

更新于

2024-04-21

许可协议

评论