前端性能优化
前端性能优化
本文主要是针对前端性能优化做一些记录。
一、性能优化的指标和工具
在分析页面渲染性能之前,先了解一下几个比较重要的指标,方便后面的理解:
FP: First Paint
(白屏时间),从用户打开网页到浏览器将网页的第一个像素渲染出来所用的时间(并不要求是有意义的内容,只是有像素开始绘制)。
FCP: First Contentful Paint
(首屏时间),是当浏览器渲染 DOM 中第一块内容元素的时间(表示的是浏览器有真实、有意义的内容渲染出来,比如文本、图像或其他可视元素)。
TTI: Time to interactive
( 第一次可交互时间),此时用户可以真正的触发 DOM 元素的事件,和页面进行交互。
FID: First Input Delay
,即用户首次与站点交互时的时间(比如当他们单击链接,点击按钮或使用自定义的 JavaScript 驱动控件时)到浏览器实际能够回应这种互动的时间。
TTFB: Time to First Byte
(首字节时间),是指从客户端开始和服务端交互到服务端开始向客户端浏览器传输数据的时间(包括 DNS、socket 连接和请求响应时间),是能够反映服务端响应速度的重要指标。
1.1 使用WebPageTest评估网站性能
WebPageTest 用来进行整体的网站质量评估、一站式性能评估。
- WebPageTest 快速入门指南快速入门指南
1.2 使用Chrome DevTools分析性能
这里简单介绍一下 chrome DevTools的几个模块:
- Network : 页面中各种资源请求的情况,这里能看到资源的名称、状态、使用的协议(http1/http2/quic…)、资源类型、资源大小、资源时间线等情况
- Performance : 页面各项性能指标的火焰图,这里能看到白屏时间、FPS、资源加载时间线、longtask、内存变化曲线等等信息
- Memory : 可以记录某个时刻的页面内存情况,一般用于分析内存泄露
- JavaScript Profiler : 可以记录函数的耗时情况,方便找出耗时较多的函数
- Layers : 展示页面中的分层情况
参考资料
二、渲染优化(浏览器)
2.1 减少重绘重排
页面解析过程:
- 解析HTML生成DOM树。
- 解析CSS生成CSSOM规则树。
- 解析JS,操作 DOM 树和 CSSOM 规则树。
- 将DOM树与CSSOM规则树合并在一起生成渲染树。
- 遍历渲染树开始布局,计算每个节点的位置大小信息。
- 浏览器将所有图层的数据发送给GPU,GPU将图层合成并显示在屏幕上。
重排
当改变 DOM 元素位置或大小时,会导致浏览器重新生成渲染树,这个过程叫重排。
重绘
当重新生成渲染树后,就要将渲染树每个节点绘制到屏幕,这个过程叫重绘。不是所有的动作都会导致重排,例如改变字体颜色,只会导致重绘。
注意,重排会导致重绘,重绘不会导致重排。
重排和重绘这两个操作都是非常昂贵的,因为 JavaScript 引擎线程与 GUI 渲染线程是互斥,它们同时只能一个在工作。
什么操作会导致重排?
- 添加或删除可见的 DOM 元素
- 元素位置改变
- 元素尺寸改变
- 内容改变
- 浏览器窗口尺寸改变
如何减少重排重绘?
- 用 JavaScript 修改样式时,最好不要直接写样式,而是替换 class 来改变样式。
- 如果要对 DOM 元素执行一系列操作,可以将 DOM 元素脱离文档流,修改完成后,再将它带回文档。推荐使用隐藏元素(display:none)或文档碎片(DocumentFragement),都能很好的实现这个方案。
2.3 高频事件防抖
防抖
防抖是指在一定时间内只会触发一次事件,如果在这段时间内再次触发,就重置时间。
防抖常见场景:
- 避免用户点击按钮(登录、发短信)太快,以致于发送了多次请求,需要防抖
- 当调整浏览器窗口大小时,resize 次数过于频繁,造成计算过多,此时需要一次到位,就用到了防抖
- 文本编辑器实时保存,当无任何更改操作一秒后进行保存
节流
节流是指在一定时间内只会触发一次事件。
节流常见场景:
- scroll 事件,滚动加载,加载更多或滚到底部监听,每隔一秒计算一次位置的操作
- input 框实时搜索并发送请求展示下拉列表,每隔一秒发送一次请求
2.4 长列表
使用分页或滚动加载,虚拟列表,移除屏外dom
三、代码优化
3.1 JS开销和如何缩短解析时间
- 优化高频事件:scroll、touchmove等事件尽量使用函数防抖节流等进行限制。。
- 合理使用requestAnimationFrame动画代替setTimeout
3.2 函数优化
- 使用纯函数
3.3 对象优化
- 尽量缓存数组长度
3.4 CSS优化
- css写在头部head,避免阻塞页面渲染
- 使用 CSS transform、opacity、will-change 等开启硬件加速
- 尽量使用css3动画
- 不滥用WEB字体:WEB字体需要下载、解析、重绘当前页面,尽量减少使用
- 避免css表达式
- 移除空置的css规则
- 避免行内style样式
3.5 HTML优化
- 压缩 HTML:将注释、空格和新行从生产文件中删除
- 减少DOM节点:DOM节点太多会影响页面的渲染
- 使用语义化标签:使用语义化标签可以提高代码的可读性和可维护性,并有助于搜索引擎优化
- 减少iframe数量
四、资源优化
4.1 资源的压缩与合并
压缩文件可以减少文件下载时间,让用户体验性更好。
4.2 图片优化
- 响应式图片:浏览器能够根据屏幕大小自动加载合适的图片。
通过 picture 实现
- <picture>
- <source srcset="banner_w1000.jpg" media="(min-width: 801px)">
- <source srcset="banner_w800.jpg" media="(max-width: 800px)">
- <img src="banner_w800.jpg" alt="">
- </picture>
- 使用 webp 格式的图片
- 图片懒加载:在页面中,先不给图片设置路径,只有当图片出现在浏览器的可视区域时,才去加载真正的图片。
- 精灵图:合并图片:当图片较多时,像精灵图可以合并为一张大图,从而减少http请求数。
- 避免img、iframe等标签的src属性为空:空src会重新加载当前页面,影响速度和效率。
- 图像尽量避免使用DataURL:DataURL图像没有使用图像压缩算法,文件会变大,并且要解码后再渲染,加载慢耗时长。
4.3 样式表和JS文件的优化
将 CSS 放在文件头部,JavaScript 文件放在底部。
- CSS 执行会阻塞渲染,阻止 JS 执行
- JS 加载和执行会阻塞 HTML 解析,阻止 CSSOM 构建
五、传输加载优化
5.1 减少HTTP请求
一个完整的 HTTP 请求需要经历 DNS 查找,TCP 握手,浏览器发出 HTTP 请求,服务器接收请求,服务器处理请求并发回响应,浏览器接收响应等过程。
瀑布流waterfall
相关概念:
Queueing
: 在请求队列中的时间。Stalled
: 从TCP 连接建立完成,到真正可以传输数据之间的时间差,此时间包括代理协商时间。Proxy negotiation
: 与代理服务器连接进行协商所花费的时间。DNS Lookup
: 执行DNS查找所花费的时间,页面上的每个不同的域都需要进行DNS查找。Initial Connection / Connecting
: 建立连接所花费的时间,包括TCP握手/重试和协商SSL。SSL
: 完成SSL握手所花费的时间。Request sent
: 发出网络请求所花费的时间,通常为一毫秒的时间。Waiting(TFFB)
: TFFB 是发出页面请求到接收到应答数据第一个字节的时间。Content Download
: 接收响应数据所花费的时间。
从这个例子可以看出,真正下载数据的时间占比为 13.05 / 204.16 = 6.39%,文件越小,这个比例越小,文件越大,比例就越高。这就是为什么要建议将多个小文件合并为一个大文件,从而减少 HTTP 请求次数的原因。
5.2 HTTTP资源缓存
为了避免用户每次访问网站都得请求文件,我们可以通过添加 Expires
或 max-age
来控制这一行为。
Expires
设置了一个时间,只要在这个时间之前,浏览器都不会请求文件,而是直接使用缓存。max-age
是一个相对时间,建议使用 max-age 代替 Expires 。
keep-alive
判断是否开启:看response headers中有没有Connection: keep-alive。开启以后,看network的瀑布流中就没有 Initial connection耗时了nginx设置keep-alive(默认开启)
- # 0 为关闭
- #keepalive_timeout 0;
- # 65s无连接 关闭
- keepalive_timeout 65;
- # 连接数,达到100断开
- keepalive_requests 100;
Cache-Control / Expires / Max-Age
:设置资源是否缓存,以及缓存时间Etag / If-None-Match
:资源唯一标识作对比,如果有变化,从服务器拉取资源。如果没变化则取缓存资源,状态码304,也就是协商缓存Last-Modified / If-Modified-Since
:通过对比时间的差异来觉得要不要从服务器获取资源
5.3 静态资源使用 CDN
内容分发网络(CDN,Content Delivery Network)是一组分布在多个不同地理位置的 Web 服务器。我们都知道,当服务器离用户越远时,延迟越高。CDN 就是为了解决这一问题,在多个位置部署服务器,让用户离服务器更近,从而缩短请求时间。
CDN 原理
如果用户访问的网站部署了 CDN,过程是这样的:
- 首先需要通过本地 DNS “迭代解析”的方式获取域名的IP地址;
- 如果本地 DNS 的缓存中没有该域名的记录,则本地 DNS 依次向根服务器、顶级域名服务器、权限服务器发出请求,得到全局负载均衡系统(GSLB)的 IP 地址;
- 本地 DNS 再向 GSLB 发出请求,GSLB 的主要功能是根据本地 DNS 的 IP 地址判断用户的位置,筛选出距离用户较近的本地负载均衡系统(SLB),并将该 SLB 的 IP 地址作为结果返回给本地 DNS;
- 本地 DNS 将 SLB 的 IP 地址发回给浏览器,浏览器向 SLB 发出请求。
- SLB 根据浏览器请求的资源和地址,选出最优的缓存服务器发回给浏览器;
- 浏览器再根据 SLB 发回的地址重定向到缓存服务器;
- 如果缓存服务器有浏览器需要的资源,就将资源发回给浏览器。如果没有,就向源服务器请求资源,再发给浏览器并缓存在本地。
参考资料:
5.4 使用 HTTP2
HTTP2 相比 HTTP1.1 有如下几个优点:
- 多路复用:在 HTTP2 上,多个请求可以共用一个 TCP 连接,这称为多路复用。同一个请求和响应用一个流来表示,并有唯一的流 ID 来标识。 多个请求和响应在 TCP 连接中可以乱序发送,到达目的地后再通过流 ID 重新组建。
- 首部压缩
- 优先级:HTTP2 可以对比较紧急的请求设置一个较高的优先级,服务器在收到这样的请求后,可以优先处理。
- 服务器推送:HTTP2 新增的一个强大的新功能,就是服务器可以对一个客户端请求发送多个响应。换句话说,除了对最初请求的响应外,服务器还可以额外向客户端推送资源,而无需客户端明确地请求。
5.5 DNS 预加载
浏览器对网站第一次的域名DNS解析查找流程依次为:浏览器缓存 ->系统缓存 ->路由器缓存 ->ISP DNS缓存 ->递归搜索。
DNS预解析的实现:
用meta信息来告知浏览器, 当前页面要做DNS预解析:
- <meta http-equiv="x-dns-prefetch-control" content="on" />
5.6 开启Gzip
Gzip即数据压缩,前端生产环境中将js、css、图片等文件进行压缩,通过减少数据传输量减小传输时间,节省服务器网络带宽,提高前端性能。
在nginx上配置开启:
- http {
- gzip on;
- gzip_static on;
- gzip_min_length 1k;
- gzip_buffers 8 16k;
- gzip_http_version 1.1;
- gzip_comp_level 6;
- gzip_types text/plain application/javascript application/x-javascript text/javascript text/css application/xml image/png image/gif image/jpeg image/ico image/jpg;
- gzip_vary on;
- gzip_proxied expired no-cache no-store private auth;
- gzip_disable "MSIE [1-6]\.";
- limit_conn_zone $binary_remote_addr zone=perip:10m;
- limit_conn_zone $server_name zone=perserver:10m;
- }
5.7 减少不必要的Cookie
Cookie存储在客户端,伴随着HTTP请求在浏览器和服务器之间传递,由于cookie在访问对应域名下的资源时都会通过HTTP请求发送到服务器,从而会影响加载速度,所以尽量减少不必要的Cookie。
六、构建优化
6.1 开启多线程
多线程可以提高程序的效率。在webpack中,可以使用 thread-loader
,它是一个可以启用多线程的加载器。
6.2 缓存加载器
在我们的项目开发过程中,Webpack 需要多次构建项目。为了加快后续构建,我们可以使用缓存,与缓存相关的加载器是缓存加载器。
- module.exports = {
- module: {
- // 如果babel-loader耗时比较长,配置cache-loader
- rules: [
- {
- test: /\.jsx?$/,
- use: ['cache-loader','babel-loader']
- }
- ]
- }
- }
6.3 文件过滤
一些文件和文件夹永远不需要参与构建。所以我们可以在配置文件中指定这些文件,防止Webpack取回它们,从而提高编译效率。
exclude
: 不需要编译的文件。include
: 需要编译的文件。
6.4 tree-shaking
tree-shaking的作用是把js文件中无用的模块或者代码删掉。
在webpack5中已经自带tree-shaking功能,在打包模式为production时,默认开启 tree-shaking功能。
- module.exports = {
- mode: 'production'
- }
6.5 base64
对于一些小图片,可以转成base64编码,这样可以减少用户的HTTP请求次数,提升用户体验。
在 webpack5,可以使用 assets-module
。如果是 webpack5之前的,可以使用 url-loader
。
- module.exports = {
- module: {
- rules: [
- {
- test: /\.(png|jpe?g|gif|webp)$/,
- type: "asset",
- parser: {
- dataUrlCondition: {
- maxSize: 10 * 1024, // 小于10kb的图片会被base64处理
- },
- },
- generator: {
- // 将图片文件输出到 images 目录中
- // 将图片文件命名 [hash:8][ext][query]
- // [hash:8]: hash值取8位
- // [ext]: 使用之前的文件扩展名
- // [query]: 添加之前的query参数
- filename: "images/[hash:8][ext][query]",
- }
- ]
- }
- }
6.6 Bundle Analyzer
可以使用 webpack-bundle-analyzer
来查看打包后的 bundle 文件的体积,然后进行相应的体积优化。
- const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
- module.exports={
- plugins: [
- new BundleAnalyzerPlugin() // 使用默认配置
- ]
- }
七、框架优化
React优化
- 使用
useMemo
缓存计算复杂的结果 和useCallback
缓存函数,避免在每次渲染时都重新计算。 - 使用
React.memo
来缓存函数组件,通过对比组件的 props 变化来避免不必要的重新渲染。 - 类组件可以使用
PureComponent
;使用shouldComponentUpdate
函数来避免类组件每次都渲染(一般用在父组件渲染而子组件无需渲染的场景)。
八、前沿优化解决方案
8.1 优化资源加载的顺序
使用 Preload
和 Prefetch
改变浏览器默认的资源加载优先级。
Preload
:提前加载较晚出现,但对当前页面非常重要的资源
字体比较特殊,需要设置 crossorigin=”anonymous”
- <link rel="preload" href="test.jpg" as="font" />
- <link
- rel="preload"
- href="https://fonts.gstatic.com/s/longcang/v15/LYjAdGP8kkgoTec8zkRgqBgxXsWsMfnCm1_q1j3gcsptb8OMg_Z2HVZhDbPBCIyx.119.woff2"
- as="font"
- type="font/woff2"
- crossorigin="anonymous"
- />
Prefetch
:提前加载后续页面或后续路由所需的资源,优先级低
- <link rel="prefetch" as="style" href="product-font.css" />
8.2 预渲染页面
8.3 使用骨架组件减少布局移动
当相关组件数据还没有完全加载时,如果样式没有控制好,会导致组件没有完全撑开,当样式加载好之后,组件的布局会发生变化,对周围的组件也会造成影响,这个性能消耗比较高,我们应该尽量避免。
骨架组件也叫 Skeleton 或 Placeholder(占位符),用来占位和提升用户感知。
九、性能优化问题面试指南
9.1 Web加载与渲染基本原理
- 首先,浏览器获取到HTML文件,然后对文件进行解析,形成DOM Tree;同时,进行CSS解析,生成Style Rules;
- 接着将DOM Tree与Style Rules合成为 Render Tree;
- 接着进入布局(Layout)阶段,也就是为每个节点分配一个应出现在屏幕上的确切坐标;
- 最后,调用GPU进行绘制(Paint),遍历Render Tree的节点,并将元素呈现出来。
参考资料:
9.2 首屏加载优化
- 代码分离,将首屏不需要的代码分离出去
- 服务端渲染或预渲染,加载完html直接渲染,减少白屏时间
- DNS prefetch,使用dns-prefetch减少dns查询时间,PC端域名发散,移动端域名收敛
- 减少关键路径css,可以将关键的css内联,这样可以减少加载和渲染时间
9.3 JavaScript内存管理
- 在变量不需要使用时,主动设置为
null
。