共计 8585 个字符,预计需要花费 22 分钟才能阅读完成。
前言
网站性能至关重要,会影响 SEO 排名、转换率、用户跳出率以及用户体验等。在浏览器中加载迟缓的网站可能会缓缓失去用户,相同可能疾速响应的网站通常会有机会获取更多流量,带来更大效益。
最近咱们在站点做了一版性能优化,把次要着陆页面的 PageSpeed 分数从本来 30 左右晋升到 80 分以上。
在这里分享下在这个过程中的一些教训,介绍下咱们是如何达成这个后果,其中又波及到哪些技术。
文章将从性能指标、性能测量与优化实际计划三个方面开展,冀望能够给大家提供一些思路与参考。
性能指标
性能指标的倒退与演进
针对线上我的项目做性能优化,首先须要有一个确定的可量化的评判规范,用以来判断优化工作是否无效。
传统的性能指标最典型的是 DOM Ready 工夫 和页面加载工夫(load time):前者指的是初始 HTML 文档被齐全加载和解析实现,个别是通过监听 DOMContentLoaded
事件取得;后者指的是整个页面所需的资源(包含脚本、款式、图片等)加载实现的工夫,通过监听全局的 load
事件获取。
在新近前后端耦合的时代,是通过在服务端应用模板引擎渲染出 HTML,能比拟好地反映网站性能。起初前端畛域的迅猛发展,尤其是随着客户端渲染计划的流行,以及各种动静技术的大量使用,这两个指标差不多曾经失去其原有的意义,无奈精确反映性能。
起初浏览器提供了 Navigation Timing API,通过 perperformance.timing
能够获取从页面开始加载到完结整个过程中不同阶段的工夫点。这很不错,开发者能够从多个维度去定义一些指标,通过简略的差值计算去监控站点性能。
比方在外部的用户行为追踪脚本(UBT)中就基于 timming API 次要定义了以下 7 个要害指标 DNS
Connect
Request
Response
Blank
Domready
Onload
DNS
(domainLookupEnd – domainLookupStart)Connect
(connectEnd – connectStart)Request
(responseStart – requestStart)Response
(responseEnd – responseStart)Blank
(domInteractive – responseStart)Domready
(domContentLoadedEventEnd – navigationStart)Onload
(loadEventEnd – navigationStart)
同样,这些指标更侧重于技术细节,并不能很好地反映用户真正关怀的问题。在做性能优化的时候,很可能面临的一种场景是,曾经把某些特定指标如加载工夫的数值大幅缩小,但用户体验依然很差。基于此,Chrome 团队和 W3C 性能工作组推出了一组 以用户为核心的性能指标,从用户角度更好地去评判页面性能。
这些次要指标蕴含:
- FCP First Contentful Paint 首次内容绘制
- LCP Largest Contentful Paint 最大内容绘制
- TTI Time to Interactive 可交互工夫
- TBT Total Blocking Time 总阻塞工夫
- CLS Cumulative Layout Shift 累积布局偏移
指标介绍
FCP
FCP 指标测量的是页面从开始加载到页面内容的任何局部在屏幕上实现渲染的工夫。“内容”能够是文本、图像(包含背景图像)、<svg>
元素或非红色的 <canvas>
元素。
这个指标答复了一个用户问题,正在运行吗。
还有一个从名称上很靠近的指标,FP(首次绘制),它们之间的区别如下:
- FP first-paint 大抵能够认为是白屏工夫
- FCP first-contentful-paint 大抵能够认为是首屏工夫
LCP
这个指标对应的要害用户问题是,是否有用,即页面是否曾经呈现出对用户有用的内容。
新近有过一些相似的指标比方 FMP(首次无效绘制),但无效绘制的定义是什么通常很难解释,而且算法经常容易出错。
相同,最大内容绘制的定义简单明了,这里的“内容”和 FCP 中的定义基本一致,指的是在可视区域内的最大图片或文本块实现渲染的工夫。
元素大小指的是内容占据的面积大小,即 size = width * height,不蕴含边距边框。
大多数状况下,页面上最吸引用户的内容往往就是最大元素,能够认为这就是页面中最重要的元素。
TTI
可交互工夫,对应的用户关注点是 能够应用吗。
晚期,对于可交互工夫始终并没有一个清晰明确的定义。刀耕火种的时代,开发者自定义工夫节点,并在代码中埋点来获取相干数据。
比方通过在 setTimeout
中放一个工作获取执行工夫点,再计算到页面开始加载的差值。
setTimeout(function() {tti = new Date() - navigationStartTime
}, 0)
而在 Lighthouse 中,可交互工夫指标有了更通用、标准化的定义。TTI 应从 FCP 工夫点开始沿时间轴查找,如果呈现 5 秒的 静默窗口(没有长工作并且不超过 2 个正着解决的 GET 申请),那么最初一个长工作介绍的工夫点即为可交互工夫。
长工作指的是执行工夫超过 50 ms 的工作。
主线程上若是存在导致阻塞状态的长工作,将导致无奈响应用户交互。
TBT
TBT 和 TTI 是一对配套指标,用于掂量在页面可交互之前的阻塞水平。
TBT 是指在 FCP 和 TTI 之间所有长工作超过 50ms 的局部的工夫总和(留神不是长工作的工夫总和)。
CLS
累积布局偏移指标用于掂量页面视觉稳定性。
单次布局偏移分数是影响分数(不稳固区域占可是区域的百分比)与间隔分数(不稳固元素最大位移间隔占比)的乘积。
CLS 指标自身始终在一直进化,便于更加精确地去掂量布局偏移对用户的影响。
其余
- SI Speed Index 速度指数 属于 Lighthouse 六大性能指标之一,应用 speedline 模块来掂量视觉进度
- TTFB Time to First Byte 首字节工夫 用于掂量服务器响应能力,所有申请包含页面、脚本 和 AJAX 等都能够统计
- FID First Input Delay 首次输出提早 因为主线程忙碌导致用户首次输出的延迟时间
- FCI First CPU Idle 首次 CPU 闲暇 与 TTI 指标类似,目前已不举荐应用
性能测量
理解了须要关注的性能指标,那应该怎么样去无效测量呢?
性能测量分两种类型,实验室测量与现场测量(实在用户监控)。有的指标只能通过实验室测量,或是只能现场测量。
实验室测量
实验室测量指的是在一个受控环境下,应用预约义的硬件设施和网络配置等规定去运行网站页面,进行性能数据采集,提取性能指标。
目前最风行的工具是 Google 的 Lighthouse,最后作为一个独立的浏览器扩大程序须要开发者自行装置(反对 Firefox),目前曾经集成到 Chrome DevTools。
Lighthouse 不仅仅是一个性能测量工具,除此之外还提供 PWA、SEO、可拜访性、最佳实际等审计报告。
在做性能优化的时候,如何无效评估优化计划的成果是一个问题,因为还没有公布到线上环境无奈采集实在用户性能数据,这时候应用工具进行实验室测量就显得至关重要。
同时,Lighthouse 提供开源 CI 工具 Lighthouse CI 开发者能自行部署服务,并集成到现有的 CI 体系中。
现场测量
现场测量,也称实在用户监控(RUM),即实时采集实在用户性能数据。
实验室测量的是在一系列特定条件下的性能数据,不能齐全反映事实世界中用户的真实情况。现场测量的劣势在于样板量足够大,包罗各种不同设施不同网络环境下的数据,从统计上更能反映真实性能状况。另一方面,现场测量需基于浏览器提供的性能 Web API,受限于以后设施采集到的数据不迭实验室测量丰盛。
定量评估的问题与计划
定量评估每一项优化计划的成果并不容易,起因包含环境差别问题,分数计算问题等。
解决方案是:
- 开发模式启动站点利用与生产模式差异较大,将利用公布到测试服务器再进行性能测量
- 本地启动 Lighthouse 进行测量,设施在不同工夫的零碎状态存在较大差别,应部署测量工具到固定服务器
- 因为环境影响单次测量的差别可能很大,基于 lighthouse NPM 包一次性跑 10 次,去除最大值和最小值之后再取中位数和平均值作为参考
- 性能分数有六大性能指标计算而来,某些指标的数值优化最终在分数上体现简直没有差别,离开看具体指标数值更正当
性能优化计划
确定优化方向,并且有了可定量评估的计划之后,接下来要做的就是如何施行具体的优化计划。
性能优化是一个陈词滥调,同时与时俱进的主题。晚期赫赫有名的 雅虎 35 条性能军规 到当初大部分依然实用,另一方面随着技术的倒退,基于上述以用户为核心的性能指标,能更有针对性地实施方案。同时借助 Lighthouse 工具,能帮忙咱们无效评估具体计划的成果。
咱们的利用是基于 React 技术栈,以下局部内容基于 React 来进行论述。
减小包体积
网站利用与传统客户端利用很不同的一点在于,利用所需资源文件都是寄存在远端服务器上的,每次拜访都有相当大的性能开销是用于资源加载。
如何让资源高效加载成了一个十分重要的问题,其中最重要的一环是网络传输,专用的 CDN 服务器蕴含就近拜访,资源缓存和压缩等性能,能节俭大量网络传输工夫,这是基础设施的角度。
从开发者的角度,首先能够对利用包体积进行瘦身。
包体积的问题次要体现在:
- 不再应用的冗余代码
- 复制粘贴的反复代码
- 非必要的大体积类库
- 未经优化的图片文件
冗余代码
冗余代码的产生有多种,比方是曾经废除不必但依然被导入的功能模块,或者是在做 AB 试验实现后未齐全移除的版本代码等。
借助相干工具,比方 Webapck 插件 webpack-bundle-analyzer
能用一种可视化的形式出现每个包的具体模块信息,大小、蕴含关系高深莫测。而 Chrome DevTools Coverage 工具能剖析出运行过程中文件(脚本和款式)的应用状况,可作为参考更好地针对性地瘦身优化。
反复代码
反复代码很大一部分是实现类似性能的过程中,间接复制粘贴一方代码进行批改导致,借助 jsinspect
能够检测到雷同和类似代码,而后进行正当形象。还有一种状况是,依赖 NPM 包提供多种形式的代码,比方 dist 目录下的打包代码,lib 目录下的 CommonJS 代码,和 es 目录下的 ES Modules 代码。若是不小心在不同中央引入不同形式的包,就等同于是引入反复功能模块。更甚一步,在跨团队单干中依赖包只提供打包版本,也会呈现 babel polyfill 代码多次重复,并且无从剖析。解决方案是制订对立的规范,举荐 NPM 包都提供仅 babel 编译不打包版本。
类库开销
在类库的应用上同样须要留神,比方仅应用一两个办法就引入整个 lodash
库,举荐做法是按需引入,不必扭转写法退出 babel-plugin-lodash
这类插件就能在代码构建时转换。另外一种状况是引入 moment
这类体积较大的库用作工夫解决与格式化,能够视理论状况采纳体积更小的替代品。对于更简略的需要,则齐全能够基于原生 API 自行实现封装一些办法。
图片文件
未经优化的图片可高达几百 KB,应在保障图片清晰度的状况压缩大小。
另一方面,为古代浏览器提供有更高效压缩算法的图片格式,相比传统的 PNG 和 JPG 格局,WebP 在等同品质下有更小的体积,留神做好降级计划。
优化资源加载
作为开发者做好包体积优化能节俭网络传输工夫,以及一部分代码执行工夫,但更重要的是让资源无效加载,可从资源加载程序和优先级方面着手。
Resource Hints
为了使页面能够疾速加载,咱们基于 PRPL 模式 进行优化。PRPL 是四个词的首字母缩写,别离代表:
- Preload 预加载最重要的资源
- Render 尽快渲染初始内容
- Pre-cache 预缓存其余资源
- Lazy load 懒加载其余路由和非关键资源
首先,咱们须要优化要害门路资源,页面中要出现的内容很多,但不是所有内容都须要第一工夫出现,优先出现最重要的内容。浏览器并不知道哪些资源是最重要的,基于 Resource Hints 能够通知浏览器资源优先级。罕用的有以下几类:
- preconnect 启动晚期连贯,包含 DNS 查找,TCP 握手等
- preload 预加载资源并缓存,以便须要时立刻应用
- prefetch 预获取资源,优先级比 preload 低,浏览器自行判断正当工夫执行操作
在应用过程须要留神:
- 不要无限度的滥用,因为其本身会耗费资源,尤其是增加了但却未应用
- 资源设置 crossorigin,对应预处理提醒也要设置,否则两者不匹配导致反复加载
Service Worker
应用 Service worker 缓存预载资源,对后续拜访会有极大的性能晋升,能节俭大量网路传输开销。
在我的项目中举荐采纳 Google 提供的 Workbox 库,能够通过配置的形式对不同类型资源利用不同缓存策略。
Service Worker 带来的优化成果不能从 PageSpeed Insights 网站上的分数间接体现,因为 PageSpeed 总是单次分数并且不应用缓存。
优化加载第三方脚本
利用依赖的第三方脚本通常会减慢页面加载速度,个别采纳以下形式:按需加载和提早加载。
按需加载
需用户交互才用到的功能模块应按需加载。举个例子,用户登录时要调用一个第三方验证模块,就没必要在页面一开始就引入该脚本,在用户执行登录操作时引入更正当。
提早加载
像是 Google analysis 和单干商营销等第三方日志埋点脚本,业务须要无奈移除,加载后占用大量性能资源。
因为自身没有依赖关系,可应用 defer script 提早脚本的解析执行。更进一步,提早到在可交互工夫之后加载就根本不会有任何影响。
组件懒加载
可视区域之外的内容,和须要用户交互时才出现的组件,都可采纳懒加载,保障页面首要内容疾速出现。
要做懒加载,首先须要正当定义拆分点进行代码宰割,而后基于动静导入和 React.lazy
即可实现。
对于大部分点击触发的组件来说,这样曾经足够,但针对页面底部可视区域之外需惯例滚动查看的内容,还要做一些额定的工作。能够自行封装实现一个组件,在外部进行判断内容是否可视,并监听 scroll
事件从新渲染。
理论中,咱们联合 react-lazyload
和 @loadable/component
实现所需性能,如下:
import React from 'react';
import loadable from '@loadable/component';
import LazyLoad from 'react-lazyload';
const LazyComponent = loadable(() => import(/* webpackChunkName: "home_lazy" */ './LazyComponent'));
export function HomePage() {
return (
<>
<MainComponenet />
<LazyLoad>
<LazyComponent />
</LazyLoad>
</>
);)
}
懒加载可能导致懒加载组件本身体验降落,可对用户比拟频繁应用的组件预加载。
适度拆分可能会产生很多体积很小的包,能够适当地进行合并。借助 webpack magic comment,配置雷同的 chunk name 能够合并打包。
import loadable from '@loadable/component';
export const SortLayer = loadable(() => import(/* webpackChunkName: "depart_select_layer" */ './SortLayer'));
export const StopLayer = loadable(() => import(/* webpackChunkName: "depart_select_layer" */ './StopLayer'));
export const TimeLayer = loadable(() => import(/* webpackChunkName: "depart_select_layer" */ './TimeLayer'));
优化渲染形式
- 服务端渲染
- 预渲染
服务端渲染
CSR(客户端渲染)的最大问题在于受用户环境影响太大,一方面是网络层面脚本文件的加载,一方面是浏览器的执行效率,不同场景下差别可能十分大。
SSR(服务端渲染)则能解决这个问题,直出 HTML 能疾速出现页面次要内容,能很好地改善 FCP 和 LCP 指标。
SSR 绝对 CSR 实质上来讲就两点:
- 将渲染(这里是指 JavaScript 执行层面的)工作转移到服务端,毕竟服务端绝对更可控
- 在首屏之前防止缩小资源网络传输,从而缩小耗时,因为网络是更不可控的一个因素
实际上,大部分时候都是联合二者,针对首屏采纳服务端渲染,让用户更快看到内容,其余仍应用客户端渲染的模式,加重服务器压力,毕竟将大量用户的渲染工作转移到服务端会是一笔不小的开销。这时,联合缓存机制能够大大节俭渲染工夫。
预渲染
基于构建时的预渲染,是应用 webpack 和 babel 等工具提前生成对应的 HTML 以及援用的脚步和款式文件。还有一种形式是基于运行时的,应用 headless 浏览器。但预渲染并不适用于有大量动静内容的页面。
优化长工作
Long Task(长工作)的定义是执行工夫超过 50 ms 的工作。咱们晓得,JavaScript 是单过程单线程的模型,主线程上一旦有耗时长的工作存在时,就会造成阻塞,无奈响应用户输出。
Long Task 跟 Lighthouse 中的两个重要性能指标 TTI 和 TBT 非亲非故,而这两个指标占比为 40%,能够说优化好 Long Task 能大幅晋升页面性能。
Long Task 可借助对应的 Long Task Web API 进行监控,开发过程中则应用 Chrome DevTools Performace 面板查看。须要留神的是,开发者的电脑配置可能很强,但用户尤其是挪动端的用户环境并没有那么乐观,应该适当调低硬件配置和网络速度,这样能发现更多的 Long Task。
工作类型有多种,除了最常见的脚本执行之外,还包含脚本解析编译、HTML 解析、CSS 解析、布局、渲染等。脚本执行是长工作的次要表现形式,这里着重阐明在 JavaScript 执行上的一些优化形式:
- requestIdleCallback API
- Web Worker
- 记忆函数
- Debounce 和 Throttle
requestIdleCallback API
针对一些不重要的工作比方埋点日志能够间接丢到 requestIdleCallback
中,浏览器会在闲暇工夫执行。在不反对的环境可应用 shim),基于 setTimeout
实现近似的性能。
库 idlize 中封装了一些十分实用的帮忙函数,应用这些办法可把工作提早到须要的时候再执行。
Web Worker
如果我的项目中的确存在比较复杂的计算,可启动 Web Worker 独自另开一个线程来计算,并应用 message
通信。
记忆函数
如果一个函数被大量调用,正当使用记忆函数一个很好的抉择,有大量的库可供咱们抉择,也能够依据应用场景自行实现。
Debounce 和 Throttle
针对 input change 和 scroll 等可能频繁触发的事件,防止无节制地调用。
React 性能优化
在 React 框架应用上有一些性能优化的实际,集体认为比拟重要的有:
shouldComponenetUpdate
useMemo
和useCallback
- 不可变数据
默认的 shouldComponenetUpdate
总是返回 true
但开发者晓得什么时候应该更新,则可自行实现该生命周期办法。举荐大部分组件都应用 pureComponent
代替,函数组件则可应用 Memo
。
useMemo
和 useCallback
都是记忆函数,可联合 Memo
防止不必要的从新渲染,或者是对低廉计算的记忆。
state 和 props 都是不可变数据,在更新深层嵌套数据应用深拷贝不是一种好形式,可借助 Immer 这类库更好地编写。
最初阐明一点,在必要的时候进行性能优化,大部分时候无需思考,而且滥用办法反而侵害性能。
缩小布局偏移
如何调试监控
有对应的 Layout Instability API 能够帮忙收集用户的布局偏移数据。
在开发调试中,Layout Shift 同样能够应用 Chrome DevTools Performance 进行剖析,能查看每一次布局偏移的分数,进行针对性优化。
罕用的优化计划有:
- 为动静元素预动态预留空间
- 图片宽高尺寸固定
预留空间可缩小其余页面元素的偏移,比方呈现在最顶部的广告位,在数据还未获取到的时候事后设置好一个容器,可防止后续大幅偏移。
针对整页动静的内容,应用骨架屏是一种很好的模式,业界已有不少成熟计划可主动生成。
设置图片宽高,则能够保障浏览器在加载图片过程中始终能调配正确的空间大小。
总结反思
借助上述中提到的性能测量形式,咱们逐渐施行优化计划并公布上线,通过近两个月断断续续的工夫,最终让性能分数稳固在 80 分左右。
性能优化也实用于二八定律,优化形式很多,只是简略地堆砌应用很可能事与愿违。不同场景下的优化计划千差万别,关键在于找准最外围的问题。以上仅提供一些思路作为参考。有些计划对特定指标成果很好,有些计划不会反映到指标分数,但有助晋升用户体验。
再者,指标掂量的是单个页面速度,而作为开发者还应掂量后续页面,从整体的维度去均衡,真正从用户角度思考。