🔁 一、加载篇
“网站页面的快速加载,能够建立用户对网站的信任,增加回访率,大部分的用户其实都期待页面能够在 2 秒内加载完成,而当超过 3 秒以后,就会有接近 40%的用户离开你的网站。”
1、首屏加载
1.1 白屏及性能优化
- 时间段:
FP(First Paint)
- performance.timing.navigationStart- 回车按下,浏览器解析网址,进行 DNS 查询,查询返回 IP,通过 IP 发出 HTTP(S) 请求
- 服务器返回 HTML,浏览器开始解析 HTML,此时触发请求 js 和 css 资源
- js 被加载,开始执行 js,调用各种函数创建 DOM 并渲染到根节点,直到第一个可见元素产生
- 优化
- loading 提示: webpack 插件
html-webpack-plugin
,配置 html 时可在文件中插入 loading 图 - 伪服务端渲染:
prerender-spa-plugin
插件在本地模拟浏览器环境,预先执行打包文件,通过解析获取首屏的 HTML,在正常环境中,返回预先解析好的 HTML - 使用 HTTP / 2.0:
- http2 采用二进制分帧的方式进行通信,而 http1.x 是用文本,http2 的效率更高
- http2 引入
多路复用
,即跟同一个域名通信,仅需要一个 TCP 建立请求通道,请求与响应可以同时基于此通道进行双向通信 - http2 可以头部压缩,能够节省消息头占用的网络的流量,而 HTTP/1.x 每次请求,都会携带大量冗余头信息,浪费了很多带宽资源
- 开启浏览器缓存
- 强缓存:表示在缓存期间不需要请求,state code 为 200
- 协商缓存:缓存过期可以使用协商缓存。协商缓存需要请求,如果缓存有效会返回 304,协商缓存需要客户端和服务端共同实现
- loading 提示: webpack 插件
1.2 FMP(First meaningful paint)
- 时间段:白屏结束后,在
FCP(first contentful paint)
和 FMP 之间。反映主要内容出现在页面上所需的时间,也侧面反映了服务器输出任意数据的速度 - 优化:
Skeleton
方法事先撑开即将渲染的元素,避免闪屏,同时提示用户。不同框架上都有相应的 Skeleton 实现,如vue-skeleton-webpack-plugin
1.3 LCP(Largest Contentful Paint)
最大内容绘制,LCP(Largest Contentful Paint),用于记录视窗内最大的元素绘制的时间,该时间会随着页面渲染变化而变化,因为页面中的最大元素在渲染过程中可能会发生改变,另外该指标会在用户第一次交互后停止记录。指标变化如下图:
LCP 其实能比前两个指标更能体现一个页面的性能好坏程度,因为这个指标会持续更新。举个例子:当页面出现骨架屏或者 Loading 动画时 FCP 其实已经被记录下来了,但是此时用户希望看到的内容其实并未呈现,我们更想知道的是页面主要的内容是何时呈现出来的。
此时 LCP 指标是能够帮助我们实现想要的需求的。
上图是官方推荐的时间区间,在 2.5 秒内表示体验优秀。
1.4 TTI(Time to Interactive)
- 时间段:当有意义的内容渲染出来后,用户会尝试与页面交互,看起来页面加载完毕了,实际 JavaScript 脚本依然在密集执行。期间绝大部分的性能消耗都在 JavaScript 的解释和执行上,这时决定 JavaScript 解析速度有两点:JavaScript 脚本体积和本身执行速度
- 优化
- JavaScript 脚本体积
SplitChunksPlugin
拆包Tree Shaking
:通过程序流分析找出代码中无用的代码并剔除。使用 ES6 模块来开启
- JavaScript 本身执行速度
- 动态加载 ES6 代码:
<script type="module">
这个标签来判断浏览器是否支持 es6 - polyfill 动态加载:引入
<script src="https://cdn.polyfill.io/v2/polyfill.min.js"></script>
即可,是否需要 polyfill - 路由级别
code splitting
:给 babel 设置plugin-syntax-dynamic-import
这个动态 import 的插件
- 动态加载 ES6 代码:
- JavaScript 脚本体积
2、组件加载
2.1 组件懒加载
懒加载:即非关键资源(屏幕可视范围外的资源)延迟加载。可降低网页首屏的 HTTP 请求数,提高首屏加载速度。
A、图像懒加载:加载页面时会先加载轻量级的占位符图像,滚动到视口时,将之替换为延迟加载的图像
- 内联图像:
Chrome 75
起支持<img>
元素loading="lazy"
属性。设置data-src
属性指向实际需加载的图像,src
指向默认的图片- 使用
Intersection Observer API
检查元素的可见性 - 使用 scroll 和 resize 事件处理程序(兼容性最好)
- 使用
- CSS 中的图像:
- 使用
Intersection Observer
检查元素可见性 - 若在可视范围内,则对元素添加
visible
类
- 使用
B、视频懒加载:
- 视频不自动播放:
<video>
元素指明preload
属性为preload="none"
- 使用
poster
属性为<video>
元素提供占位符poster="placeholder.jpg"
- 视频代替动画 GIF:使用
Intersection Observer
处理 - 懒加载库:Lozad.js
C、组件懒加载:运用 react 的lazy
、Suspense
实现组件懒加载。在服务端渲染中尚不可用
const Lazycomponent1 = React.lazy(() => import("./lazy.component1.js"));
const Lazycomponent2 = React.lazy(() => import("./lazy.component2.js"));
const Lazycomponent3 = React.lazy(() => import("./lazy.component3.js"));
function AppComponent() {
return;
<div>
<Suspense fallback={<div>loading ...</div>}>
<LazyComponent1 />
<LazyComponent2 />
<LazyComponent3 />
</Suspense>
</div>;
}
//引入多个 lazy load 组件,并指定同一个 Suspense fallback 来处理 loading 状态
2.2 组件预加载
预加载(preload): 提供了一种声明式的命令,让浏览器提前加载指定资源(关键的脚本、样式、字体、主要图片等),在需要执行的时候再执行。在页面加载后需要立即使用资源时使用。目前
firefox
、IE
不支持
- 使用 link 标签:如
<link rel="preload" href="/style.css" as="style" />
"as"
属性帮助浏览器安排优先级,值可为style、script、font、fetch
- 在使用 preload 获取字体时添加 crossorigin 属性
preload
告诉浏览器页面必定需要的资源,浏览器一定会加载这些资源
🖕 二、执行篇
1、动画性能优化
1.1 CSS3 动画优化
- 将动画放在一个独立图层,避免动画效果影响其他渲染层的元素
- 尽量避免回流和重绘
- 尽量使用 GPU,速度更快
1.2 Canvas 动画优化
- 运用
requestAnimationFrame
- 能将所有动画放到一个浏览器重绘周期里去做,保存
CPU
循环次数 - 运行时浏览器会自动优化方法的调用,页面非激活状态动画会自动暂停
- 离屏
canvas
:把离屏canvas
当成一个缓存区。把需重复绘制的画面数据缓存起来,减少调用canvas
的API
消耗 - 避免浮点运算
- 减少调用
Canvas API
web workers API
:在后台运行脚本的方法,与网页隔离。遇到大规模的计算,可以通过此 API 分担主线程压力
2、大量数据性能优化
2.1 虚拟列表(Virtual List)
虚拟列表指的就是
可视区域渲染
的列表。实现虚拟列表就是处理滚动条滚动后的可见区域的变更。——处理大量数据的渲染环节
2.2 Web Worker
利用
Web Worker
进行多线程编程——处理大量的数据计算环节
- 当主线程在处理界面事件时,worker 可以在后台运行处理大量的数据计算,将计算结果返回给主线程,由主线程更新 DOM 元素
- 和主线程通过
onmessage
和postMessage
接口进行通信
🛶三、重新渲染篇
React 组件 props 或 state 改变,都会重新渲染。渲染即意味着更新dom,具体 render 时通过 diffing(对比)、reconciliation(协调) 来比较虚拟dom、协调更新必要的dom。然而,当不必要的重新渲染次数过多,对比协调过程可能会拖慢应用,从而出现性能问题。
1、React 函数组件减少不必要的 render 优化
组件每次更新渲染前会调用shouldComponentUpdate()方法。记忆(memoizing)组件(函数组件包裹于React.memo或类组件扩展于React.PureComponent)即是shouldComponentUpdate()方法的一种实现。
React的组件重新渲染(re-render),一般有以下几种情况,优化主要关注第三四种情况:
- 组件自己状态(state)改变,重新渲染;
- 组件依赖的上下文(context)改变,重新渲染;
- 父组件重新渲染,子组件接收父组件的参数(props)变化,子组件重新渲染
- 父组件重新渲染,子组件接收父组件的参数(props)不变,子组件重新渲染;
1.1 React.memo
如果React.memo包装了一个函数组件,它将记住渲染的输出结果,并在state,props 或 context 未更改的情况下跳过后续的渲染。
React.memo只检查 props 的更改。 如果组件的 state 或 context 发生更改,即使 props 没有更改,也将重新渲染组件。
React.memo only checks for prop changes. If your function component wrapped in React.memo has a useState, useReducer or useContext Hook in its implementation, it will still rerender when state or context change.
React.memo对 props 进行了浅比较。 如果要控制比较,可提供自定义比较函数作为第二个参数。
- React.memo仅作为性能优化存在,不应依赖它阻止渲染。使用前后需性能度量确定是否值得增加复杂性。
- 不建议使用:组件重新渲染不会太影响性能;比较函数执行较昂贵;
- 建议使用:组件渲染一次消耗很多性能
function MyComponent(props) {
/* render using props */
}
function areEqual(prevProps, nextProps) {
/*
return true if passing nextProps to render would return
the same result as passing prevProps to render,
otherwise return false
*/
}
export default React.memo(MyComponent, areEqual);
因此针对组件第四种情况,如果父组件传递的Props不变,但父组件自己需要重新渲染,而子组件渲染一次很昂贵,不需要不必要的重新渲染,可以使用React.memo包裹子组件。
1.2 React.useCallBack
把函数以及依赖项作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,这个 memoizedCallback 只有在依赖项有变化的时候才会更新
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
因此,针对组件第三种情况,父组件将函数作为props传递给子组件时,函数作为一个引用,每一次父组件渲染,子组件接收的函数也会变化,在父组件用useCallBack后可以控制函数是否变更。
1.3 React.useRef
若需在 state 或 context 更新的时候阻止当前组件渲染,证明这个属性不适合作为 state、context,而应该作为静态属性或者放在 class 外面作为一个简单的变量 。「ref」 对象是一个 current
属性可变且可以容纳任意值的通用容器,类似于一个 class 的实例属性。
- ref 的值在组件重新渲染之间保持不变;
- 更新 ref 不会触发组件重新渲染;
- ref 更新必须在useEffect()回调内部或在处理程序(事件处理程序,计时器处理程序等)中进行更新
其与state的不同之处:
- state状态更新是异步更新的,会 schedule 安排一个状态更新,在组件下一次渲染后拿到最新的值;而ref 更新是同步更新的,能实时拿到最新的值
- ref 用于存储组件的基础结构数据,而 state 用于存储直接在屏幕上呈现的信息
1.4 React.useReducer
1.5 合理拆分组件
合理拆分组件,可以控制更小粒度的更新。如果整个页面只有一个大的组件,那么当 props 或者 state 变更之后,需要 reconciliation 的是整个组件。
- 向下移动 state :当一部分的 state 可以单独抽离出去作为另一个子组件时,就不必在父组件设置 state,也就不会影响到父组件中其他子组件的重新渲染。
- 内容提升:当一部分 state 某子组件依赖,父组件也依赖因而不能单独抽离出去时。不依赖的部分就依然放在另一子组件中,然后以JSX内容的形式传递给父组件,也被称为children属性。
四、参考资料
1、《还在看那些老掉牙的性能优化文章么?这些最新性能指标了解下》
2、《写在 2021 的前端性能优化指南》
3、https://www.webpagetest.org/
4、《Chrome DevTools Performance》