作者 | Addy Osmani
譯者 | 許學(xué)文
策劃 | 蔡芳芳
轉(zhuǎn)發(fā)鏈接:https://mp.weixin.qq.com/s/ciIl4Cg9ZULDUY77Yuot0A
本文最初發(fā)布于Addy Osmani博客,經(jīng)原作者授權(quán)由 InfoQ 中文站翻譯并分享
今天我將向大家演示如何使用 React Profiler API、Tracing API 以及 User Timing API 來分別追蹤 React 的組件渲染、用戶交互以及自定義性能指標(biāo)。
React Profiler API
首先來了解下 React Profiler,它主要用來追蹤應(yīng)用組件的 渲染過程 以及渲染開銷,同時(shí)標(biāo)記出應(yīng)用的性能瓶頸。Profiler 接受一個(gè) onRender 回調(diào)函數(shù),當(dāng)被追蹤的組件以及子代組件發(fā)生更新時(shí),該函數(shù)就會(huì)被調(diào)用。下圖是在影片排期應(yīng)用中使用 Profiler 追蹤各個(gè)組件渲染:
Profiler 中 onRender 回調(diào)函數(shù)的具體參數(shù)如下:
- id:: 這是 Profiler 的唯一標(biāo)示,區(qū)分是哪個(gè) Profiler 追蹤的組件樹發(fā)生了更新
- phase: 如果更新是掛載階段這個(gè)值就是“mount”,如果是二次渲染階段就是“update”
- act ualDuration: 更新花費(fèi)的渲染時(shí)間
- baseDuration: 更新預(yù)計(jì)花費(fèi)的渲染時(shí)間
- startTime: 更新開始時(shí)間點(diǎn)
- commitTime: 更新提交的時(shí)間點(diǎn)
- interactions: 更新中包含的交互信息
const callback = (id, phase, actualTime, baseTime, startTime, commitTime) => {
console.log(`${id}'s ${phase} phase:`);
console.log(`Actual time: ${actualTime}`);
console.log(`Base time: ${baseTime}`);
console.log(`Start time: ${startTime}`);
console.log(`Commit time: ${commitTime}`);
}
運(yùn)行上面的代碼,在 Chrome 調(diào)試器中可以看到如下輸出:
也可以打開 React DevTools,在 Profiler 面板中可以看到組件渲染的時(shí)間火焰圖:
切換到排序視圖
當(dāng)然也可以使用多個(gè) Profiler 來分別追蹤應(yīng)用中的各個(gè)不同的部分,示例代碼如下:
import React, { Fragment, unstable_Profiler as Profiler} from "react";
render(
<App>
<Profiler id="Header" onRender={callback}>
<Header {...props} />
</Profiler>
<Profiler id="Movies" onRender={callback}>
<Movies {...props} />
</Profiler>
</App>
)
知道了如何追蹤組件渲染,那么如果想跟蹤交互,該怎么做?
交互追蹤 Tracing API
想一下,如果能追蹤到交互(例如:按鈕的點(diǎn)擊),那么在回答“這個(gè)按鈕點(diǎn)擊花費(fèi)了多少時(shí)間更新 DOM?”這樣的問題時(shí)是不是就有了依據(jù)。要感謝 Brian Vaughn 的努力,React 在其 調(diào)度包 中引入了對這個(gè)功能的試驗(yàn)支持,更詳細(xì)的說明可以點(diǎn)擊 這里 查看。
一個(gè)交互追蹤,需要包含一個(gè)描述(例如:添加購物車按鈕被點(diǎn)擊)、一個(gè)時(shí)間戳和一個(gè)回調(diào)函數(shù),在回調(diào)函數(shù)中你可以定義一些和該交互相關(guān)的邏輯。在“影片排期應(yīng)用”中就有一個(gè)添加電影到播放列表的“+”號按鈕,這個(gè)就是一個(gè)交互按鈕。
下面的代碼演示了如何追蹤這個(gè)按鈕的點(diǎn)擊行為:
import { unstable_Profiler as Profiler } from "react";
import { render } from "react-dom";
import { unstable_trace as trace } from "scheduler/tracing";
class MyComponent extends Component {
addMovieButtonClick = event => {
trace("Add To Movies Queue click", performance.now(), () => {
this.setState({ itemAddedToQueue: true });
});
};
在 React 開發(fā)調(diào)試工具的 interaction 面板中可以看到具體的交互行為和持續(xù)時(shí)間:
這個(gè) API 同樣也可以 追蹤初始化渲染:
import { unstable_trace as trace } from "scheduler/tracing";
trace("initial render", performance.now(), () => {
ReactDom.render(<App />, document.getElementById("app"));
})
Brian 提供了更多的例子,比如如何追蹤異步行為等。這些示例都在其“React 中進(jìn)行交互追蹤”項(xiàng)目的 gist 中。
Puppeteer 的使用
如果想對 UI 交互追蹤腳本做進(jìn)一步了解的話,你可能會(huì)對 Puppeteer 這個(gè)庫感興趣。Puppeteer 是一個(gè) Node 庫,基于 Chrome 開發(fā)協(xié)議封裝 API 來操作 headless Chrome(譯者注:Chrome 瀏覽器對無界面形態(tài))。
為了捕獲 DevTools 對當(dāng)前運(yùn)行程序性能的追蹤,Puppeteer 提供了 trace .start() 和 trace.stop() 兩個(gè) API,下面我們就用它來追蹤按鈕點(diǎn)擊的過程,代碼如下::
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
const navigationPromise = page.waitForNavigation();
await page.goto('https://react-movies-queue.glitch.me/')
await page.setViewport({ width: 1276, height: 689 });
await navigationPromise;
const addMovieToQueueBtn = 'li:nth-child(3) > .card > .card__info > div > .button';
await page.waitForSelector(addMovieToQueueBtn);
// 開始追蹤...
await page.tracing.start({ path: 'profile.json' });
// 按鈕點(diǎn)擊
await page.click(addMovieToQueueBtn);
// 停止追蹤
await page.tracing.stop();
await browser.close();
然后在開發(fā)工具的性能面板中導(dǎo)入 profile.json,我們就可以看到當(dāng)按鈕點(diǎn)擊的時(shí)候,所有函數(shù)的調(diào)用情況:
如果你對交互追蹤感興趣并且想了解更多的話,不妨看看 Stoyan Stefanov 的“JAVAScript 組件級別的 CPU 開銷”這篇文章。
客戶端性能追蹤 API
使用 客戶端性能追蹤 API 可以追蹤一些定制的性能指標(biāo),并且時(shí)間精確度會(huì)更高。它有 2 個(gè)主要的 API:
- window.performance.mark(): 存儲當(dāng)前 mark 執(zhí)行時(shí)的時(shí)間戳
- window.performance.measure(): 存儲 2 個(gè)相同 mark 之間的執(zhí)行時(shí)間
示例代碼如下:
// 記錄任務(wù)開始之前的時(shí)間戳
performance.mark('Movies:updateStart');
// 這里執(zhí)行了一些任務(wù)...
// 記錄任務(wù)結(jié)束的時(shí)間戳
performance.mark('Movies:updateEnd');
// 計(jì)算任務(wù)開始前后的差值
performance.measure('moviesRender', 'Movies:updateStart', 'Movies:updateEnd'
當(dāng)你通過 Chrome 調(diào)試工具中的性能面板查看一個(gè) React 應(yīng)用時(shí),有一個(gè)“Timings”的區(qū)域,這里歸集了你的 React 組件的執(zhí)行時(shí)間。在渲染時(shí),React 會(huì)把通過客戶端 API 得到的性能數(shù)據(jù)發(fā)布到這里。
注意:React 在它的開發(fā)包中用 Profiler 替代了 User Timings,不過由于 User Timings 的時(shí)間精度更高,所以可能會(huì)在未來的 3 級規(guī)格的瀏覽器中重新添加它。
在互聯(lián)網(wǎng)上,你會(huì)發(fā)現(xiàn)有一些其他的 React 應(yīng)用已經(jīng)在使用 User Timing 追蹤他們的 自定義指標(biāo),包括 Reddit 網(wǎng)站中的“到第一標(biāo)題可見花費(fèi)的時(shí)間”和 Spotify 網(wǎng)站中的“到回放準(zhǔn)備完畢花費(fèi)的時(shí)間”。
還可以在 Chrome 調(diào)試器的 Lighthouse 面板 中查看到定制化的 User Timing 標(biāo)記和追蹤方法,如下圖:
在 Next.js 的最近版本中也針對一些事件 添加 了很多 User timing 標(biāo)記和追蹤,例如:
- Next.js-hydration: 混合持續(xù)時(shí)間
- Next.js-nav-to-render: 導(dǎo)航開始到開始渲染之間的時(shí)間
所有的這些追蹤都可以在調(diào)試器的 Timings 區(qū)域看到:
對比 DevTools 和 Lighthouse
值得注意的是,Lighthouse 和 Chrome 調(diào)試工具 中的性能面板都可以深入分析 React 應(yīng)用程序的加載和運(yùn)行時(shí)性能,用戶可以看到下面這些性能指標(biāo):
React 用戶可能會(huì)喜歡像 總阻塞時(shí)間 (TBT) 這樣的新指標(biāo),它量化一個(gè)頁面具體什么時(shí)候才可以交互(可 交互時(shí)間), 下面我們可以看下在并發(fā)模式前后應(yīng)用發(fā)生更新時(shí),TBT 的情況:
這些工具一般能幫助我們了解在瀏覽器級別的視圖性能瓶頸,例如,哪些 繁重冗長的任務(wù) 會(huì)引起交互延遲 (例如按鈕點(diǎn)擊響應(yīng)) :
Lighthouse 還為一些特定的性能場景提供了修改建議。如在 Lighthouse 6.0 中可以看到一個(gè)提示,建議我們移除 未使用的 JavaScript代碼。Lighthouse 追蹤到了這個(gè)問題并且提醒我們可以使用 React.lazy () 來引入這個(gè) JavaScript。
借助用戶端的硬件進(jìn)行性能智能檢查,往往對性能分析非常有幫助。
最后,除了上面提到的我通常還會(huì)從 RUM 和 CrUX 獲取一些數(shù)據(jù)字段,然后用 webpagetest.org/easy 工具幫我生成更多的場景圖片,以便更好的進(jìn)行性能分析。






