Author:Hongying Guo
Editor:Xiaoli Tan
背景
我们开发了一个浏览器插件,用xigua~代称,用于运营同学等便捷的查看视频的一些数据,该插件目前存在很多重复渲染的问题,最近打算对 xigua~ 项目进行进行一下组件渲染优化,想先用 React Devtools 插件 对当前的该插件进行性能检测和代码调试,便于总结优化前后的收益。
但是,打开 Components 调试工具会发现 Dom 树只有一个原页面的根结点,无法检测到插件的 Dom 树。
补充一下React Devtools 的 icon 的颜色含义:
蓝色:生成环境 红色:开发环境 灰色:非 React 项目 并且,在西瓜 PC 站是,React Devtools 的 icon 是蓝色的。
在 Youtube 页面时,React Devtools 的 icon 是灰色的。
这说明,React Devtools 插件并没有检测到插件的上下文。
为什么不能对插件进行调试?
Devtools 插件 API
Devtools 插件可以通过以下的几组 API 分别对面板、被审查窗口、网络请求进行操作或监听:
- chrome.devtools.panels:面板相关;
- chrome.devtools.inspectedWindow:获取被审查窗口的有关信息;
- chrome.devtools.network:获取有关网络请求的信息;
React Devtools 插件
在页面加载的时候,React Devtools插件会设置一个名为__REACT_DEVTOOLS_GLOBAL_HOOK__ 全局变量 hook,然后 React 初始化的时候就是通过这个这个 hook 与插件进行通信的。
如下面的源码,如果检测到当前页面的类型是 html 就会插入一个包含 script 标签,包含了初始化__REACT_DEVTOOLS_GLOBAL_HOOK__的代码。
if ('text/html' === document.contentType) {
injectCode( // injectCode 即 插入 script 标签的逻辑
';(' +
installHook.toString() + // installHook中会定__REACT_DEVTOOLS_GLOBAL_HOOK__
'(window))' +
saveNativeValues +
detectReact,
);
}
Object.defineProperty(
target,
'__REACT_DEVTOOLS_GLOBAL_HOOK__',
({
configurable: __DEV__,
enumerable: false,
get() {
return hook;
},
}: Object),
);
源码中,是通过检查全局变量__REACT_DEVTOOLS_GLOBAL_HOOK__是否存在以及是否有 render 实例来判断当前页面是否包含 react。
chrome.devtools.inspectedWindow.eval(
'window.__REACT_DEVTOOLS_GLOBAL_HOOK__ && window.__REACT_DEVTOOLS_GLOBAL_HOOK__.renderers.size > 0',
function(pageHasReact, error) {
if (!pageHasReact || panelCreated) {
return;
}
}
// ....
}
从上面的逻辑中可以看到,Devtools 始终是与 window 进行交互,window 即当前页面的命名空间,而 Devtools 是无法与 Buddy 插件的命名空间进行通信的。
图中可以看到 Devtools 是可以和 Inspected Window 和 Background Page 进行通信的,无法与 Content Script 进行通信,这也是为什么 React Devtools 无法对 Buddy 插件的页面进行调试与性能检测的原因。
Content Script 插件如何调试?
- 使用开发者工具的 Performance,React 在开发环境会自主上报每个组件的渲染耗时。这种方案比较适合对项目的整体性能查看,当需要对某一个具体的组件进行渲染检测的时候不是很方便。
- 使用 React 的实验性 API Profiler,如下,通过使用 Profiler 组件包裹并定义 onRender 的回调函数,自主的打印组件的渲染时间,来测试组件的渲染耗时。
import React, { unstable_Profiler as Profiler } from 'react';
export default function TestComp () {
function onRenderCallback (
id, // 发生提交的 Profiler 树的 “id”
phase, // "mount" (如果组件树刚加载) 或者 "update" (如果它重渲染了)之一
actualDuration, // 本次更新 committed 花费的渲染时间
baseDuration, // 估计不使用 memoization 的情况下渲染整颗子树需要的时间
startTime, // 本次更新中 React 开始渲染的时间
commitTime, // 本次更新中 React committed 的时间
interactions // 属于本次更新的 interactions 的集合
) {
console.log(`${id}组件渲染时间:`, actualDuration);
console.log(`${id}组件不使用Memo估计渲染时间:`, baseDuration);
}
return (
<Profiler id="XgVideo" onRender={onRenderCallback}>
<div>
// 组件内容
</div>
</Profiler>
)
}
结果如图:
Profiler 的原理是使用 Performance.measure() API 来实现上报标记的。 例如,通过下面的代码,我们也可以自主上报信息到 开发者工具的 Performance:
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
<script type="text/javascript">
performance.mark("测试上报Performance-start");
// 等待一些时间。
setTimeout(function() {
// 标志时间的结束。
performance.mark("测试上报Performance-end");
// 测量两个不同的标志。
performance.measure(
"测试上报Performance",
"测试上报Performance-start",
"测试上报Performance-end"
);
// 清除存储的标志位
performance.clearMarks();
performance.clearMeasures();
}, 1000);
</script>
</body>
</html>
补充:禁止 React Devtools 改变 state 和 props
在生产环境也可以通过 React Devtools 插件对 state 和 props 进行更改并且生效,这里可能会存在一定的安全问题,可以通过在使用 React 之前运行下面的代码来实现在生成环境一定程度上禁止修改 state 和 props。
if (process.env.NODE_ENV == 'production' && typeof window.__REACT_DEVTOOLS_GLOBAL_HOOK__ === 'object') {
__REACT_DEVTOOLS_GLOBAL_HOOK__.inject = function() {};
}