减少 http 请求数量
不同协议 下 请求数量 仍然可能成为 请求/响应 慢的原因:
- 合并公共资源,如 雪碧图 等
- 内置模块资源,如 生成
base64
图片、通过symbol
引用svg
等 - 合并代码块,如构建工具分包策略配合 公共组件封装、组件复用逻辑抽离 等
- 按需加载资源,如 路由懒加载、图片懒加载、上拉加载、分页加载 等
减少不必要的 cookie
不必要的 cookie
来回传输会造成带宽浪费:
- 减少
cookie
存储的内容 - 对于静态资源采用
CDN
托管(即非同域),不同域名默认不携带cookie
CDN 托管静态资源 + HTTP 缓存
CDN
加速的本质是缓存加速,将服务器上存储的静资源容缓存在 CDN
节点上,当后续访问这些静态内容时,无需访问服务器源站,选择就近访问 CDN
节点即可,从而达到加速的效果,同时减轻服务器源站的压力。
在掘金中体现如下:
协议升级为 Http2.0
http1.x
存在的问题:HTTP
的底层协议是TCP
,而TCP
是面向连接即需要 三次握手 才能建立连接,其中:
http1.0
中使用的是 短连接,即 一次请求/响应 结束后就会断开连接,这个过程比较耗时http1.1
中使用的是 长连接,在 请求/响应头 中设置Connection: keep-alive
即可开启,优点是 长连接 允许多个请求共用一个TCP
连接,缺点是带来了 队头阻塞:
- 每个
TCP
连接中的多个请求,需要进行排队,只有队头的请求被响应,才能继续处理下一个请求 - 其中一个缓解方案就是如果当前
TCP
连接中发生 队头阻塞,那就将部分请求放到其他TCP
连接中 - 浏览器一般会限制同一个域名建立
6-8
个TCP
链接,这也就是为什么需要为应用划分子域名、静态资源托管CDN
的原因之一
http1.x
中header
部分的内容可能会很大,而且每一个请求可能都需要携带大量 重复header
的 文本内容,而这些也是导致 请求/响应 慢的原因之一
以上问题
http2.0
都能够解决:
- 针对 TCP 连接数 被限制的问题,
http2.0
采用 多路复用 一个域名只对应一个TCP
连接 - 针对 http 队头阻塞 问题,
http2.0
中通过二进制分帧层为每个 请求/响应 添加stream id
保证 请求/响应 一一对应,即不必等待前面的请求处理完成,并且还可以为每个请求添加 优先级 - 针对
header
数据大的问题,http2.0
中传输的header
帧经过处理后会用 二进制 的方式表示,替换了原本的 文本格式,并使用HPACK
算法进行压缩
- 接收/发送 两端会维护一个 索引表,通过下标来标识
header
,针对后续重复的header
信息就可以用对应的索引来代替
- 针对传统的 请求 —> 响应 模式,
http2.0
中提供了 服务端推送 的能力,让服务端能够主动向客户端推送关键资源,加快资源加载
比如在掘金中的体现:
如何保证视图更快的渲染和交互?
保证资源快速到达客户端后,接下来就需要针对 浏览器的解析和渲染 进行优化,当然还包括后续的页面交互的优化,这其实就是 浏览器层面 的优化。
浏览器渲染 HTML
文件的核心过程:
- HTML 解释器:将
HTML
文档经过词法分析输出DOM Tree
- CSS 解释器:解析
CSS
文档,生成样式规则CSSOM
- 样式计算:将
DOM Tree
和CSSOM
合并生成Render Tree
- 布局计算:计算
Render Tree
节点在页面中的坐标位置,创建Layout Tree
- 划分图层:页面中有很多复杂的效果,如一些复杂的
3D
变换、页面滚动,或使用z-index
做z
轴排序等,为了更加方便地实现这些效果,渲染引擎还需要为特定的节点生成专用的图层,生成对应的图层树Layer Tree
- 图层绘制:
- 染引擎实现图层的绘制时,会把一个图层的绘制拆分成很多小的 绘制指令,并将这些指令按照顺序组成一个 待绘制列表
- 当图层的绘制列表准备好后,主线程 会把 待绘制列表 提交(
commit
)给 合成线程
- 栅格化 raster:
- 由于视口有限,用户只能看到页面的很小一部分,没必要绘制出所有图层内容,因此 合成线程 会将 图层(layer) 划分为 图块(tile)
- 渲染进程 把 生成图块的指令 发送给
GPU
并执行生成图块的 位图
- 合成和显示:
- 一旦所有图块都被光栅化,合成线程 就会生成一个绘制图块的命令 ——
DrawQuad
,然后将该命令提交给 浏览器进程 - 浏览器进程 里面有一个 viz 组件,会根据
DrawQuad
命令,将其页面内容绘制到内存中,最后再将内存显示在屏幕上
渲染层面
减少阻塞渲染的因素
真正渲染视图之前,必然要生成 DOM Tree
和 CSSOM
,因此必须保证 HTML 解释器 和 CSS 解释器 都尽早处理完成,同时 JavaScript
的加载和执行可能会阻塞这个过程:
HTML
文档中首次渲染的节点数量要尽量少,避免深层次的嵌套结构,避免大量使用慢标签(如:iframe
)等CSS
资源放文档头部,降低CSS
复杂度,比如 合理使用CSS
选择器JavaScript
资源放文档底部,合理使用defer、async
的加载方式
懒加载
懒加载主要是针对数量大、资源加载慢的情况,比如图片资源、大量列表数据展示等:
- 图片资源:优先加载在可视区范围内的图片,可视区外的图片
延后加载
,或者说当移入的可视区时再加载 - 列表数据:列表数据通常数据里量大,不可能一次渲染完所有数据,一般通过
分页加载、上拉加载
等方式分批次渲染
白屏优化
白屏是由于 SPA
应用需要等待 JavaScript
加载并执行完成后才会生成具体的页面结构内容导致的,即初始化模板中没有任何有意义需要被渲染的 HTML
结构:
- 添加 白屏
loading
,可在模板中添加默认的loading
效果,等到真正页面内容被渲染就可以替换loading
内容 - 添加 骨架屏,和上述方案一致,在真正页面内容展示出来之前,先展示默认的视图内容,避免白屏
服务端渲染(server-side rendering)
现代框架默认是属于客户端应用框架,即组件的代码会在浏览器中运行,然后向页面输出 DOM 元素,也叫 客户端渲染(client-side rendering,CSR):
- 优点
用户体验更好
,基于 前端路由 的方式并不会真正进行 页面跳转,即不会使页面重新刷新、加载,带来更高的流畅度占用服务端资源少
,CSR 渲染 是交由客户端进行处理,服务端不需要关心渲染计算的过程,减轻了服务端的压力
- 缺点
"白屏" 时间较长
,主要是因为 CSR 渲染需要*.js
的支持,而*.js
又必须保证*.html
被接收和解析,*.html
又强依赖于当前的 网络环境,因此,在差网环境下回导致 白屏时间过长,特别是在移动网络环境下对 SEO 的支持不友好
,因为 白屏时间较长 导致在一段时间内没有重要的内容能够交由 搜索引擎 进行分析、分类、打标签等,并且 搜索引擎 并不会等待页面渲染完成,因此对 SEO 优化并不友好
服务端渲染(server-side rendering,SSR) 可将相同组件在服务渲染成相应的 HTML
字符串,并发送给浏览器进行渲染,即客户端不需要等待所有的 JavaScript 都被下载并执行之后才显示,所以用户可以更快看到完整的渲染好的内容。
预渲染(prerender)
上述 服务端渲染(server-side rendering,SSR) 虽然能够解决一些客户端存在的问题,但它也带来了别的问题:
需要保证开发一致性
,比如 服务端 和 客户端 能够执行的组件生命周期钩子不同,一些外部库在 服务端渲染 应用中可能需要经过特殊处理需要更多的构建设定和部署要求
,一个完全静态的 SPA 可以部署在任意的静态文件服务器,但服务端渲染应用需要一个能够运行 Node.js 服务器的环境更多的服务端负载
,在 Node.js 中渲染一个完整的应用,会比仅供应静态文件产生更密集的 CPU 运算,并且需要考虑访问流量过大的情况等
因此,并不是所有应用都合适 服务端渲染,如果只是希望通过 SSR 来改善一些 推广页面 (如 /
、/about
、/contact
等) 的 SEO,那么应该优先考虑 预渲染 的方式:
- 预渲染 是在打包构建过程中(离屏状态),针对对应的
routes
路由预先生成对应的页面内容 - 预渲染 需要和 打包构建工具(webpack、rollup 等) 进行配合,如
webpack
,就可通过prerender-spa-plugin
来支持 预渲染
交互层面
减少回流/重绘
重绘:页面中元素样式的改变并不影响它在文档流中的位置时(如:color、background-color、visibility
等),浏览器会将新样式赋予给元素并 重新绘制
回流:当 Render Tree
中部分或全部元素的 尺寸、结构、某些属性 发生改变时,浏览器 重新渲染 部分或全部文档
- 减少对
DOM
进行频繁操作 - 使经常变动的元素脱离文档流,如具有持续性的动画效果,会一直触发回流和重绘
- 避免访问或减少访问会导致浏览器 强制刷新队列 的属性,如:
offsetTop、offsetLeft、offsetWidth
等
- 【扩展】浏览器的渲染队列机制会通过 队列 将会触发 回流或重绘 的操作进行存储,等到一定的时间或一定的数量时再执行这些操作
- 避免对
css
进行单个修改,如在JavaScript
修改多个样式时,尽量使用css
选择器实现样式的集中变更 - 使用
will-change
开启GPU
加速,will-change
指定的属性使得浏览器可在元素属性真正发生变化之前提前做好对应的优化 - 预先设定图片尺寸,避免图片资源加载完成后引发回流
防抖/节流
防抖:多次频繁触发执行操作,以 最后一次 为准,忽略中间过程
节流:在指定的时间间隔内,只允许 执行一次对应的操作
合理使用 防抖/节流
优化应用中的操作,比如 节流
可用于优化 滚动事件、模糊搜索等,防抖
可用于优化一些按钮点击操作等。
Web Worker
JavaScript
是单线程的,如果存在需要大量计算的场景(如视频解码),UI
线程就会被阻塞,甚至浏览器直接卡死。
Web Worker
可以使脚本运行在新的线程中,它们独立于主线程,可以进行大量的计算活动,而不会影响主线程的 UI
渲染,但不能滥用 Web Worker
。
虚拟列表
最常用的还是 分页加载 的方式:
- 基于
table
表格的渲染,只会渲染固定数量的DOM
- 基于
上拉加载
列表的渲染,随着加载数据的增多,对应的DOM
节点也会增多,达到某个限制页面一定会发生卡顿
虚拟列表 核心就是固定渲染的 DOM
数,通过动态切换数据内容实现视图的更新,并保证文档中真实 DOM
的数量不随着数据量增大而增大(其实和 table
分页很像,但它支持滚动)。
想了解其核心实现的,可查看 虚拟滚动是怎么做性能优化的?
大文件分片上传
大部分的项目总少不了文件上传功能,但对大文件的上传还是有必要进行优化,所谓的 断点续传、秒传 都要基于 分片上传 这个核心功能。
想了解其核心实现的,可查看 请问:怎么实现大文件快速上传?
Excel 导入/导出
针对 Excel 导入/导出 的功能相信很多人第一印象是后端的活,但大多数情况下,后端接口的处理速度会受各种影响,导致速度方面不是很理想,有时候也是需要前端来进行优化处理的,比如导入时前端不发送文件只发送解析后的 JSON
数据,导出时不需要单独发送额外接口,直接使用当前展示数据实现导出等。
想了解其核心实现的,可查看 给我实现一个前端的 Excel 导入和导出功能
Vue 项目的优化
这部分内容相信大家都不陌生,下面就简单列举一些内容(包括但不限于):
- 减少响应式数据的生成,对于纯展示、又需要使用在
template
模板中使用的数据,可使用Object.freeze()
进行冻结,避免被转为 不必要的响应式数据 Vue
组件初始化是比较损耗性能的,使用 函数式组件 减少组件初始化的过程,适用于实现没有业务逻辑只展示内容的简单组件- 合理使用
v-show
和v-if
、为v-for
组件设定唯一key
(非index
)、v-for
和v-if
不要一起使用等 - 使用
KeepAlive
复用组件,避免组件重复的创建、销毁带来的性能损耗 - 使用
() => import(xxx)
方式实现路由懒加载 - 使用
ESM
的方式封装自定义工具库等 - 针对第三方库做到按需引入
- 合理使用闭包,避免造成内存泄漏
- 及时清除组件中的副作用,比如
setTimeout、setInterval、addEventListener
等
基于 vue.config.js
或 webpack
进行优化,具体可见 如何优化你的 vue-cli 项目?
总结
以上优化方案对应到 上一篇 中提到的性能指标,如下:
- 首字节达到时间(
Time to First Byte,TTFB
) - 首次绘制(
First Paint,FP
) - 首次内容绘制(
First Contentful Paint,FCP
) - 首屏时间 / 最大内容绘制(
Largest Contentful Paint, LCP
) - 累积布局偏移(
Cumulative Layout Shift, CLS
) - 首次输入延迟(
First Input Delay, FID
)
关键资源越早到达客户端,证明 TTFB
时间越短,而这也能间接的减少 FP
和 FCP
的时间;对资源进行了压缩处理意味着能够尽可能提升 LCP
的时间;减少了页面的 回流/重绘 就能使得 CLS
的数值越小,视图越趋于稳定;FID
是一个用于跟踪浏览器对用户输入做出反应之前的延迟时间的指标,包括点击和敲击,保证资源的快速加载和页面尽早渲染,其对应的数值就越小,视图响应就越快。
最后
前端性能优化 的范围实在太大,以上列举的优化主要围绕着 资源加载、页面渲染/交互 两个大的方向,而具体的优化方案其实有很多(包括但不限于上述内容),很多内容随着关注的方向不同而不同。
【补充】很多人会认为上述的内容偏八股,但其实是在划分大的方向,例如看到了 preload、prefetch 就应该自己去查询如何使用(看看已有的库),理解大家最想要实际的 0 —> 1 式的教学方案,但每个人的项目终究不同,性能优化仍需要你在自己的项目中找到对应的瓶颈点,然后根据的瓶颈点去结合上述的优化方向去实操!!!