前端性能优化

什么是 web 性能?

MDN 上的 web 性能定义: Web 性能是网站或应用程序的客观度量和可感知的用户体验

  • 减少整体加载时间: 减少文件体积、减少 HTTP 请求、使用预加载
  • 使得网站尽快可用:仅加载首屏内容,其他内容根据需要进行懒加载
  • 平滑和交互性:使用 CSS 替代 JS 动画、减少 UI 重绘
  • 感知表现:你的页面可能不难做到更快,但你可以让用户感觉更快。耗时操作要给用户反馈,比如加载动画、进度条、骨架屏等提示信息
  • 性能测定:性能指标、性能测试、性能监控持续优化

如何进行 Web 性能优化?

  1. 首先需要了解性能指标 - 多快才算快?
  2. 使用专业的工具可量化的评估出网站或应用的性能表现
  3. 然后立足于网站页面响应的生命周期,分析出造成较差性能表现的原因
  4. 最后进行技术改造、可行性分析等具体的优化实践
  5. 迭代优化

指标

RAIL 性能模型

R(Response) - 尽快响应用户,应该在 100ms 内快速响应用户输入
A(Animation)- 在展示动画的时候,每一帧应该以 16ms 进行响应,避免卡顿
I(Idle)- 当使用 JS 主线程的时候,应该把任务划分到执行时间小于 50ms 的片段中去,这样可以释放线程以进行用户交互
L(Load)- 应该在小于 1S 的时间内加载完成你的网站,并进行用户交互

基于用户体验的性能指标

FCP (First Contentful Paint) 首次内容绘制

浏览器首次绘制来自 DOM 的内容的时间,内容必须是文本、图片(包含背景图)、非白色的 canvas 或 svg,也包括带有正在加载中的 web 字体的文本。

白屏时间在 0-2 秒,属于快速,2-4 秒是中等,超过 4 属于慢。

LCP ( Largest Contentful Paint) 最大内容绘制

可视区域中最大的内容元素呈现到屏幕上的时间,用以估算页面的主要内容对用户可见时间。

网站应力争使用 2.5 秒或更短的“最大内容绘画”,2.5 - 4 秒是中等,超过 4S 是慢。

FID( First Input Delay) 首次输入延迟

从用户第一次与页面交互(例如单击链接、点击按钮等)到浏览器实际能够响应应该交互的时间
输入延迟是因为浏览器的主线程正忙于做其他事情,所以不能响应用户。发生这种情况的一个常见原因是浏览器正忙于解析和执行应用程序加载的大量计算的 JS。

100ms 内是快,100-300ms 是中等,大于 300ms 是慢

TTI (Time to Interactive)

网页第一次完成达到可交互状态的时间点,浏览器已经可以持续性的响应用户的输入。完全达到可交互状态的时间点是在最后一个长任务完成的时间,并且在随后的 5 秒内网络和主线程是空闲的。

超过 50ms 的任务是长任务,0-3.8 是快,3.9-7.3S 是中等,7.3S 以上是慢。

TBT (Total Block Time) 总阻塞时间

度量 FCP 和 TTI 之间的总时间。

0-300:快; 300-600:中等; >600:慢

CLS (Cumulative Layout Shift) 累计布局偏移

CLS 会测量在页面整个生命周期中发生的每个意外的布局移位的所有单独布局移位分数的总和,它是一种保证页面的视觉稳定性从而提升用户体验的指标方案。

0-0.1:快; 0.1-0.25: 中等; >0.25:慢

Web Vitals

Web Vitals 是一组统一的质量衡量指标 - Core Web Vitals,其中包括加载体验、交互性和页面内容的视觉稳定性。这是 Google 提出的,可以节省学习成本和时间,不需要学习其他诸多的指标。

将其他很多性能指标简化成这三个指标: FCP、LID、CLS

web 性能测试

灯塔 Lighthouse

可以打开浏览器开发者工具调试,会给出分数,分析报告,和推荐解决方案。这个分数不一定准确,有时候受网络和硬件设备影响,并且要设置清除缓存。

WebPageTest

是一个在线网站测试工具,https://www.webpagetest.org/。输入网站网址,在云端测试,支持多地区、浏览器、设备测试,可以设置网络速度,分辨率、测试次数等等,会给出一个很详细的报告。

Chrome DevTool

前端页面的生命周期

如何缩短首屏速度?

效果比较好的操作

  1. 减少首屏资源体积
  • 打包工具的压缩
  • 异步加载(分析能够异步加载的,比较体积大,但又不是马上需要的功能,和首屏渲染没关系)
  • 更新为体积更小的新版本(把一些老版本的库替换成新版本的支持 tree-shaking)
  • 编写代码尽量减少体积
  • 去除大的 base64 体积(打包工具默认将图片转成 base64,小图片可以转,但是 大图片和媒体资源 建议不要转成 base64)
  • 能不用第三方库就不用第三方库(eg:时间格式化)

效果不太好或者在特殊情况下的优化操作

  1. 首屏数据尽量并行,如果可行让小数据量接口合并到其他接口
  2. 页面包含大量 dom 可以分批随滚动渲染
  3. 骨架屏,loading,先让屏幕不白,减少用户焦虑

操作速度和渲染速度

什么情况下会造成操作卡顿和渲染慢?

1. 一次性 操作大量

长列表渲染和异步渲染

长列表渲染

  1. 虚拟化列表技术
    只渲染可见区域的数据项,而不是渲染整个列表
  2. 分页加载
    将长列表分割成多个页面,一次性只加载当前页面的数据,而不是全部数据。当用户滚动或者翻页时,再加载下一页的数据
  3. 使用列表项重用
    保持列表项的可重用性,避免频繁地创建和销毁 DOM 元素。这可以通过使用列表项池来维护已渲染的列表项,然后根据需要更新其内容。

异步渲染

1. prefetch 加载

假设 page1,page2 是同步加载,page3 是异步加载(prefetch),首屏打开都会给这个三个创建 link 标签加载,但是没用标记 prefetch 的 link 会先加载,然后首屏展示完之后,再加载 prefetch 资源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const routes = [
{
path: "/",
name: "Home",
component: Home,
},
{
path: "/about",
name: "About",
component: () => {
return import("../views/About.vue");
},
},
];

如果将网速调慢, 从 network 中也可以看出来,虽然先创建了 about.js 请求,但是需要等其他的请求完成之后再加载的 about

2. script 加载

App.js(page1,page2)->首页加载完成->进入 page3->执行 js,创建一个 script 标签引入 page3 相关 js->page3.js->page3 页面看到

prefetch 和 script 加载 对比:

script 加载:

  1. 做到了充分按需引入,用到的时候在加载,不用永不加载,充分节省了带宽
  2. 最大问题是,切换需要等待,体验不是很流畅

prefetch 加载:

  1. 充分利用使用者不占用带宽的浏览时间,切换到异步加载的页面是可能早已经加载好的,用户体验更流畅
  2. 一些本次行为不会打开的页面也会加载,一定程度上浪费了带宽

优化

  1. 按需引入 import {read, utils} from 'xlsx'
  2. 一些非马上使用的操作,改成异步
1
2
3
4
5
6
7
8
change(){
import('jquery').then(res=>{
let $=res.default
let a =$('.test')
a.html("hello")
})
}

3. 利用 setTimeout 或 requestAnimationFrame

在渲染大量数据时,可以使用 setTimeoutrequestAnimationFrame 来将渲染任务分解成多个小任务,并给浏览器一些空闲时间来处理其他任务,从而减少卡顿和提高渲染性能。

4. 使用 Web Workers

可以将一些耗时的计算或处理任务放在 Web Workers 中进行,以避免阻塞主线程,从而提高页面的响应性能

5. 懒加载

对于某些不是立即需要的数据或组件,可以延迟加载,直到用户需要时再进行加载和渲染。这可以通过 React 中的 React.lazy() 和 Vue 中的 vue-router 的懒加载功能来实现。

6. 使用分片加载

对于大型数据集,可以将数据分成多个片段进行加载,并在每个片段加载完成后进行渲染。这样可以降低单次操作的负担,提高渲染的效率

2. 进行了复杂度高的运算(eg:循环)

循环中操作尽量精简(可能效果不明显)

3. vue 和 react 项目中,不必要的渲染太多

vue

vue 中有依赖收集, 配合上 vue3 的静态节点标记,已经基本上避免了因为数据改变引起的无意义渲染

  1. 频繁切换的显隐内容用 v-show 也就是 display 来控制隐藏,只有打开就一次性决定显示与否的用 v-if 不去创建
  2. 循环,动态切换内容加好 key 值
  3. keep-alive 缓存
  4. 区分请求粒度,减少请求范围,也能减少更新

react

  1. PureComponentReact.memo来减少不必要的重新渲染
  2. 通过实现shouldComponentUpdate生命周期方法来手动控制是否进行组件重新渲染。
  3. 避免在render方法中直接修改 state 或 props,因为这会触发组件的重新渲染。而是应该使用setState来更新state
  4. 避免在渲染函数中执行昂贵的操作,例如大量计算、数据请求等,这会影响渲染性能。可以将这些操作移到生命周期方法中,或者使用异步渲染技术进行优化。

数据缓存

  • 不变数据,定期时效可以缓存在 cookies 或者 localStorage 中,比如 token,用户名
  • 可以考虑做一个缓存队列,存于内存中(全局对象,vuex)。这样能保证刷新就更新数据,也能一定程度上缓存数据