可视化前端性能瓶颈
性能是网页用户体验的核心部分。性能不佳时,用户转化率就会降低。
量化 Web 性能的方法有很多,但原理都是一样的。首先,测量以获得基准,然后分析结果,最后尝试修复。这个循环可以重复,直到你得到一个满意的新基准。
最重要的是衡量用户真正关心的事情。我将向您展示如何分析和改进运行缓慢的 JavaScript 代码。我们将借助 Chrome 开发者工具来完成这项工作。
我将介绍一些浏览器 API,用于标记和测量代码。我们将使用一个小型演示应用程序,以便我们随时进行分析和改进。
先决条件
如果您想跟随演示进行操作,请阅读以下内容。否则,请跳过本节!
您将需要安装以下软件:
- git
- npm
- 您选择的代码编辑器
了解 JavaScript、React 以及对 Chrome DevTools 有基本的了解也会很有帮助
设置演示
git clone https://github.com/richiemccoll/visualising-front-end-performance-demo.git
git checkout before-fixes
npm i
npm start
这将为您打开一个新的浏览器窗口,其外观应类似于:
用户计时 API
我们需要讨论的第一件事是如何测量运行缓慢的代码。
浏览器在窗口上提供了一个名为window.performance的接口。我们可以使用它来获取当前页面的性能信息。在本演示中,我们将重点介绍两种方法。
窗口.性能.标记()
顾名思义,这个 API 允许我们在运行缓慢的函数中插入开始和结束标记。标记其实就是一个带有关联名称的时间戳。
我们可以像这样使用它:
function createUser() { | |
window.performance.mark('createUser-start'); | |
// ... | |
window.performance.mark('createUser-end'); | |
return user; | |
} |
function createUser() { | |
window.performance.mark('createUser-start'); | |
// ... | |
window.performance.mark('createUser-end'); | |
return user; | |
} |
窗口.性能.测量()
此 API 允许我们在两个标记(起始和结束)之间创建一个度量。度量也是一个带有关联名称的时间戳。
创建此度量将有助于我们在开发者工具中可视化函数。如果您忘记添加它,您将看不到任何内容👀。
我们可以像这样使用它:
function createUser() { | |
window.performance.mark('createUser-start'); | |
// ... | |
window.performance.mark('createUser-end'); | |
window.performance.measure( | |
'createUser-measure', | |
'createUser-start', | |
'createUser-end' | |
); | |
return user; | |
} |
function createUser() { | |
window.performance.mark('createUser-start'); | |
// ... | |
window.performance.mark('createUser-end'); | |
window.performance.measure( | |
'createUser-measure', | |
'createUser-start', | |
'createUser-end' | |
); | |
return user; | |
} |
这就是我们现在需要从 window.performance 中介绍的全部内容,但我建议查看MDN上的完整 API 。
分析UI
我们将在开发模式下运行此演示。一般来说,最好在生产版本上运行测量。这样做的原因之一是,库往往会删除生产环境不需要的代码。例如,开发者警告。这可能会影响测量结果,因此需要注意这一点。
我们将分析的功能是更改 SpaceX 发射的顺序(从最早到最新)。如果您已启动并运行演示,请尝试点击按钮来更改顺序。现在打开 Chrome DevTools 并切换到“性能”选项卡。
如果您不熟悉这个界面,可能会觉得有点难懂。这个链接可以帮助您了解如何使用它。
让我们将 CPU 节流选项更改为 6 倍减速,然后尝试单击该按钮几次。
你注意到有什么不同吗?感觉有点迟钝。如果我们在点击此按钮的同时点击“录制”,我们就可以看到浏览器实际在做什么。
这里发生了很多事情。点击按钮后,主线程上的 JavaScript 活动(黄色块)会大幅增加。JavaScript 完成后,样式和布局(深紫色块)会运行。然后,浏览器会将更新内容绘制到屏幕上(绿色小块)。
我们还看到了一个很棒的性能指标真实案例——React 的用户计时。它仅在开发模式下可用,不建议依赖。React Profiler 是衡量 React 性能的最佳工具,我将在以后的文章中介绍它。
获取基线
我们要做的第一件事是通过标记起点和终点来获取基线测量值。让我们在按钮的onClick事件处理程序中创建起点标记。
在调用setOrder之前打开src/components/LatestLaunches.js
并添加它。
<button | |
onClick={() => { | |
performance.mark("changingOrder-start"); | |
setOrder(); | |
}} | |
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded" | |
> | |
Click to view {order} launches | |
</button> |
<button | |
onClick={() => { | |
performance.mark("changingOrder-start"); | |
setOrder(); | |
}} | |
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded" | |
> | |
Click to view {order} launches | |
</button> |
有了这些,我们现在要标记结束并创建一个度量。我们首先要知道的是顺序何时发生了变化。一种方法是将上一次渲染的 order prop 值与当前渲染顺序值进行比较。如果不同,则标记结束。
我们可以使用名为 的自定义钩子将前一次渲染的值存储在 ref 中usePrevious
。
function usePrevious(value) { | |
const ref = useRef(); | |
useEffect(() => { | |
ref.current = value; | |
}); | |
return ref.current; | |
} |
function usePrevious(value) { | |
const ref = useRef(); | |
useEffect(() => { | |
ref.current = value; | |
}); | |
return ref.current; | |
} |
要使用这个自定义钩子,我们需要创建一个新的效果,该效果将在LatestLaunches组件渲染完成后运行。这意味着我们将从测量结果中看到浏览器总共完成了多少工作。
function LatestLaunchList({ launches, setOrder, order = "older" }) { | |
const prevOrder = usePrevious(order); | |
useEffect(() => { | |
if (prevOrder && prevOrder !== order) { | |
performance.mark("changingOrder-end"); | |
performance.measure( | |
"changingOrder-measure", | |
"changingOrder-start", | |
"changingOrder-end" | |
); | |
} | |
} | |
}, [order, prevOrder]); | |
// ... | |
} |
function LatestLaunchList({ launches, setOrder, order = "older" }) { | |
const prevOrder = usePrevious(order); | |
useEffect(() => { | |
if (prevOrder && prevOrder !== order) { | |
performance.mark("changingOrder-end"); | |
performance.measure( | |
"changingOrder-measure", | |
"changingOrder-start", | |
"changingOrder-end" | |
); | |
} | |
} | |
}, [order, prevOrder]); | |
// ... | |
} |
现在切换回 Chrome DevTools,点击记录并再次开始单击该按钮!
这changingOrder-measure
是我们的第一个基准。我们将努力提升这个数字。在我的机器上,我看到的大约是800 毫秒。
请记住:我们引入了一些最少的仪器工作来获取测量值(usePrevious 自定义钩子),因此我们从测量中排除了该测量值的持续时间。
修复 # 1 并测量
我们先从容易实现的地方开始。我们可以防止 React 重复渲染 Card 组件太多次。React 提供了一个开箱即用的实用程序,名为 ,memo
我们可以使用它。
让我们打开src/components/Card.js
并导入它。
import React, { memo } from "react"; |
import React, { memo } from "react"; |
然后我们可以通过传入我们想要记忆的组件来使用它。
export default memo(Card); |
export default memo(Card); |
现在让我们切换回 DevTools,进行另一次记录并查看这些更改如何影响我们的基线。
新的基准在600-700 毫秒之间。这仍然不太好。那么我们还能做些什么呢?
让我们一步一步思考一下当我们点击按钮改变顺序时实际发生了什么。
- 我们告诉 Launch 商店更新其内部订购状态。
- 然后,React 将这个新值作为 props 接收。React 运行协调算法来更新卡片的顺序。
- 然后浏览器必须运行 Style 来重新计算每张卡片已更改的样式。
- 随着卡片的变化,浏览器会运行布局来计算每个卡片的大小和位置。
- 然后浏览器会将排序更新绘制到屏幕上。
这些步骤中一个共同的因素是卡的数量。这也是我们下一批性能修复应该关注的地方。
让我们看看 DOM 中有多少个 Card 元素。
提示:快速执行此操作的方法是打开 DevTools 中的“元素”选项卡。右键单击包含卡片的 div 元素,并将其存储为全局变量。访问 childElementCount 属性会显示 DOM 中有 96 张卡片。
从 UI 角度来看,任何时候都大约有 5 到 10 张卡片可见。这也意味着我们不需要 DOM 中存在 96 张卡片。
虚拟化
有一种常见的渲染技术可以缓解这个问题。这个概念被称为“列表虚拟化”或“窗口化”。本质上,在任何给定时间渲染的 DOM 元素数量只是列表的一小部分。当用户滚动时,“窗口”会移动,并随之更新屏幕上的内容。
有多个库提供了这种开箱即用的技术。例如:
我决定选择masonic
这个演示,因为开始只需要最少的自定义实现。
修复 #2 并测量
让我们导入 Masonry 组件src/components/LatestLaunches.js
。
import { Masonry } from "masonic"; |
import { Masonry } from "masonic"; |
让我们改变呈现卡片列表的方式。
<div className="flex flex-wrap -m-4"> | |
<Masonry | |
items={launches} | |
columnWidth={300} | |
render={({ data: launch }) => { | |
return ( | |
<Card | |
name={launch.name} | |
details={launch.details} | |
image={launch.imgUrl} | |
url={launch.url} | |
date={launch.date} | |
/> | |
); | |
}} | |
/> | |
</div> |
<div className="flex flex-wrap -m-4"> | |
<Masonry | |
items={launches} | |
columnWidth={300} | |
render={({ data: launch }) => { | |
return ( | |
<Card | |
name={launch.name} | |
details={launch.details} | |
image={launch.imgUrl} | |
url={launch.url} | |
date={launch.date} | |
/> | |
); | |
}} | |
/> | |
</div> |
是时候再录制一些内容并点击按钮了。让我们切换回 Chrome DevTools。
太棒了🔥。现在我们减少了 DOM 元素的数量,情况开始好转了。基准时间现在大约在70-150 毫秒。使用虚拟化技术成功缩短了半秒的工作时间。
结论
当然,我们可以进行更多的优化,使这个基线数字变得更小,但我将把这留给读者作为练习。
关键在于理解测量、分析和修复的周期。对于前端性能问题,我们可以使用 User Timings API 来实现。
如果您有兴趣了解更多信息并希望更深入地了解网络性能,请阅读以下链接。
如果您有任何问题或意见,请联系我们。
链接和归属
- https://developer.mozilla.org/en-US/docs/Web/API/Performance
- https://developers.google.com/web/tools/chrome-devtools/evaluate-performance/reference
- https://github.com/facebook/react/pull/18417
- https://web.dev/virtualize-long-lists-react-window/
- https://reactjs.org/blog/2018/09/10/introducing-the-react-profiler.html