Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
000e6511e6 | ||
|
|
a652284bbc |
@@ -0,0 +1,133 @@
|
||||
## 1 引言
|
||||
|
||||
Error Boundaries 是 React16 提出来用来捕获渲染时错误的概念,今天我们一起读一读 [A Simple Guide to Error Boundaries in React](https://alligator.io/react/error-boundaries/) 这篇文章,了解一下这个重要机制。
|
||||
|
||||
## 2 概述
|
||||
|
||||
Error Boundaries 可以用来捕获渲染时错误,API 如下:
|
||||
|
||||
```jsx
|
||||
class MyErrorBoundary extends Component {
|
||||
state = {
|
||||
error: null,
|
||||
};
|
||||
|
||||
static getDerivedStateFromError(error) {
|
||||
// 更新 state,下次渲染可以展示错误相关的 UI
|
||||
return { error: error };
|
||||
}
|
||||
|
||||
componentDidCatch(error, info) {
|
||||
// 错误上报
|
||||
logErrorToMyService(error, info);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.error) {
|
||||
// 渲染出错时的 UI
|
||||
return <p>Something broke</p>;
|
||||
}
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `static getDerivedStateFromError`: 在出错后有机会修改 state 触发最后一次错误 fallback 的渲染。
|
||||
- `componentDidCatch`: 用于出错时副作用代码,比如错误上报等。
|
||||
|
||||
这两种方法中任意一个被定义时,这个组件就会成为 `Error Boundary` 组件,可以阻止子组件渲染时报错。
|
||||
|
||||
最后作者还提出一个建议,建议将 Error Boundary 单独作为一个组件,而不是将错误监听方法与业务组件耦合,一方面考虑到复用,另一方面则因为错误检测只对子组件生效。
|
||||
|
||||
好吧,其实 React 官方文档比这篇文章介绍的详细的多得多,原文介绍到此结束。
|
||||
|
||||
## 3 精读
|
||||
|
||||
[React Error Boundaries 官方文档](https://reactjs.org/docs/error-boundaries.html) 里提到了四种无法 Catch 的错误场景:
|
||||
|
||||
1. 回调事件。由于回调事件执行时机不在渲染周期内,因此无法被 Error Boundary Catch 住,如有必要得自行 try/catch。
|
||||
2. 异步。比如 `setTimeout` 或 `requestAnimationFrame`,和第一条同理。
|
||||
3. 服务端渲染。
|
||||
4. Error Boundary 组件自身触发的错误。因为只能捕获其子组件的错误。
|
||||
|
||||
这也是使用 Error Boundaries 最容易有疑问的地方。除了上面的情况,笔者结合自身经验再列举几种异常边界场景。
|
||||
|
||||
### 无法捕获编译时错误
|
||||
|
||||
很明显,即便是 React 官方 API `Error Boundary` 也只能捕获运行时错误,而对编译时错误无能为力。
|
||||
|
||||
编译时错误包括不限于编译环境错误、运行前的框架错误检查提示、TS/Flow 类型错误等,这些都是 `Error Boundary` 无法捕获的,而且没有更好的办法 Catch 住,遇到编译错误就在编译时解决吧,仅关注运行时错误就好了。
|
||||
|
||||
### 可以作用于 Function Component
|
||||
|
||||
虽然函数式组件无法定义 `Error Boundary`,但 `Error Boundary` 可以捕获函数式组件的错误,因此可以曲线救国:
|
||||
|
||||
```jsx
|
||||
// ErrorBoundary 组件
|
||||
class ErrorBoundary extends React.Component {
|
||||
// ...
|
||||
}
|
||||
|
||||
// 可以捕获所有组件异常,包括 Function Component 的子组件
|
||||
const App = () => {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<Child />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### 对 Hooks 也可生效
|
||||
|
||||
对于 Hooks 中异常也可以生效,比如下面的代码:
|
||||
|
||||
```jsx
|
||||
const Child = (props) => {
|
||||
React.useEffect(() => {
|
||||
console.log(1);
|
||||
props.a.b;
|
||||
console.log(2);
|
||||
}, [props.a.b]);
|
||||
|
||||
return <div />;
|
||||
};
|
||||
```
|
||||
|
||||
要注意的是,出现在 deps 中的错误会立即被 Catch,导致 `console.log(1)` 都无法打印。但如果是下面的代码,则可以打印出 `console.log(1)`,无法打印出 `console.log(2)`:
|
||||
|
||||
```jsx
|
||||
const Child = (props) => {
|
||||
React.useEffect(() => {
|
||||
console.log(1);
|
||||
props.a.b;
|
||||
console.log(2);
|
||||
}, []);
|
||||
|
||||
return <div />;
|
||||
};
|
||||
```
|
||||
|
||||
所以 React 官网的这句话并不是指 `Error Boundary` 对 Hooks 不生效,而是指 `Error Boundary` 无法以 Hooks 方式指定,对功能是没有影响的:
|
||||
|
||||
> componentDidCatch and getDerivedStateFromError: There are no Hook equivalents for these methods yet, but they will be added soon.
|
||||
|
||||
所以这里的理解要注意一下,另外 React 官方文档 [Hooks FQA](https://reactjs.org/docs/hooks-faq.html#how-do-lifecycle-methods-correspond-to-hooks) 有很多宝藏,建议抽时间逐条阅读。
|
||||
|
||||
## 4 总结
|
||||
|
||||
`Error Boundary` 可以捕获所有子元素渲染时异常,包括 render、各生命周期函数,但也有很多使用限制,希望你可以正确使用它。
|
||||
|
||||
错误捕获也不是万能的,更多时候我们要避免并及时修复错误,通过错误捕获降低出错时对用户体验的影响,并在第一时间内监控起来并快速修复。
|
||||
|
||||
最后,你有明明正确使用了 `Error Boundary` 却依然无法 Catch 住的错误 Case 吗?
|
||||
|
||||
> 讨论地址是:[精读《React Error Boundaries》 · Issue #246 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/246)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
@@ -0,0 +1,199 @@
|
||||
## 1 引言
|
||||
|
||||
在数据中台做 BI 工具经常面对海量数据的渲染处理,除了组件本身性能优化之外,经常要排查整体页面性能瓶颈点,尤其是维护一些性能做得并不好的旧代码时。
|
||||
|
||||
React 性能调试是面对这种问题的必修课,借助 [Profiling React.js Performance](https://addyosmani.com/blog/profiling-react-js/) 这篇文章一起学习一下这个技能吧。
|
||||
|
||||
## 2 精读
|
||||
|
||||
本文介绍了众多性能检测工具与方法。
|
||||
|
||||
### React Profiler
|
||||
|
||||
`Profiler` 这个 API 是一种运行时 Debug 的补充,可以通过其 callback 拿到组件渲染信息,用法如下:
|
||||
|
||||
```jsx
|
||||
const Movies = ({ movies, addToQueue }) => (
|
||||
<React.Profiler id="Movies" onRender={callback}>
|
||||
<div />
|
||||
</React.Profiler>
|
||||
);
|
||||
|
||||
function callback(
|
||||
id,
|
||||
phase,
|
||||
actualTime,
|
||||
baseTime,
|
||||
startTime,
|
||||
commitTime,
|
||||
interactions
|
||||
) {}
|
||||
```
|
||||
|
||||
这个 callback 会在每次渲染时执行,渲染分为初始化和更新阶段,通过 `phase` 区分,下面是参数详细说明:
|
||||
|
||||
- id: 传入的 id。
|
||||
- phase: "mount" 或 "update",表示更新状态。
|
||||
- actualDuration: 实际渲染耗时。
|
||||
- baseDuration: 没有使用 memo 时的渲染预计耗时。
|
||||
- startTime: 开始渲染的时间。
|
||||
- commitTime: React 提交更新的时间
|
||||
- interactions: 何种原因导致的渲染,比如 `setState` 或 hooks changed 之类。
|
||||
|
||||
注意尽量不要轻易使用 `Profiler` 检测性能,因为 `Profiler` 本身也会消耗性能。
|
||||
|
||||
如果不想获得这么详细的渲染耗时,或者不想提前在代码中埋点,可以利用 DevTools 的 Profiler 查看更直观更简洁的渲染耗时:
|
||||
|
||||
<img width=400 src="https://img.alicdn.com/tfs/TB1sPAuDuL2gK0jSZPhXXahvXXa-1846-1028.png">
|
||||
|
||||
其中 Ranked 可以展示按照渲染耗时排序后的结果,Interations 需要配合 Tracing API 使用,在后面会提到。
|
||||
|
||||
### Tracing API
|
||||
|
||||
利用 `scheduler/tracing` 提供的 `trace` API,我们可以记录某个动作的耗时,比如 “点击添加按钮收藏一个电影” 耗时多久:
|
||||
|
||||
```jsx
|
||||
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 });
|
||||
});
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
在 Interations 中可以看到动作触发的耗时:
|
||||
|
||||
<img width=400 src="https://img.alicdn.com/tfs/TB1XR.FDAY2gK0jSZFgXXc5OFXa-1846-1010.png">
|
||||
|
||||
这个动作还可以是渲染,比如可以记录 ReactDOM 渲染的耗时:
|
||||
|
||||
```jsx
|
||||
import { unstable_trace as trace } from "scheduler/tracing";
|
||||
|
||||
trace("initial render", performance.now(), () => {
|
||||
ReactDom.render(<App />, document.getElementById("app"));
|
||||
});
|
||||
```
|
||||
|
||||
<img width=300 src="https://img.alicdn.com/tfs/TB18hyHfcKfxu4jSZPfXXb3dXXa-1846-740.png">
|
||||
|
||||
甚至还可以追踪异步的耗时:
|
||||
|
||||
```jsx
|
||||
import {
|
||||
unstable_trace as trace,
|
||||
unstable_wrap as wrap,
|
||||
} from "scheduler/tracing";
|
||||
|
||||
trace("Some event", performance.now(), () => {
|
||||
setTimeout(
|
||||
wrap(() => {
|
||||
// 异步操作
|
||||
})
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
有了 `Profiler` 与 `trace` 这两件武器,我们可以监控任意元素的渲染耗时与交互耗时,几乎可以涵盖所有性能监控需要。
|
||||
|
||||
### Puppeteer
|
||||
|
||||
我们还可以利用 Puppeteer 实现自动化操作并打印报告:
|
||||
|
||||
```jsx
|
||||
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);
|
||||
|
||||
// Begin profiling...
|
||||
await page.tracing.start({ path: "profile.json" });
|
||||
// Click the button
|
||||
await page.click(addMovieToQueueBtn);
|
||||
// Stop profliling
|
||||
await page.tracing.stop();
|
||||
|
||||
await browser.close();
|
||||
})();
|
||||
```
|
||||
|
||||
首先利用 `puppeteer` 创建一个浏览器,新建一个页面并打开 `https://react-movies-queue.glitch.me/` 这个 URL,等待页面加载完毕后利用 DOM 选择器找到按钮,利用 `page.click` API 模拟点击这个按钮,并在前后利用 `page.tracing` 记录性能变化,并将这个文件上传到 DevTools Performance 面板,就会得到一份自动的性能检测报告:
|
||||
|
||||
<img width=400 src="https://img.alicdn.com/tfs/TB1623EDxz1gK0jSZSgXXavwpXa-2769-2289.png">
|
||||
|
||||
这张图相当重要,是浏览器综合运行开销分析的利器,最上面分为 4 个部分:
|
||||
|
||||
- FPS:每秒帧数,绿色竖线越高表示 FPS 越高,出现红线则表示出现了卡顿。
|
||||
- CPU:CPU 资源,用面积图展示消耗 CPU 资源的事件。
|
||||
- NET:网络消耗,每条横杠表示一种资源的加载。
|
||||
- HEAP:内存水位,由于短时间内看不出来是否会内存溢出,一般只用来简单看看内存消耗是否符合预期,对于内存溢出的检测需要用持续监控上报的方式。
|
||||
|
||||
下面会有一张 Network 详细图解,比如这张图:
|
||||
|
||||
<img width=400 src="https://img.alicdn.com/tfs/TB1D.wKDxD1gK0jSZFyXXciOVXa-2868-750.png">
|
||||
|
||||
细线表示等待的时间,粗线表示实际加载的情况,其中浅色部分表示服务器等待时间,即从发送下载请求到服务器响应第一个字节的时间。这部分可以看出资源并行加载阻塞情况以及资源服务器响应时间是否存在问题。
|
||||
|
||||
Timings 展示了几个重要时间节点,这里列举一部分:
|
||||
|
||||
- FP:First Paint,第一次绘制。
|
||||
- FCP:First Contentful Paint,第一次内容绘制。
|
||||
- LCP:Largest Contentful Paint,最大内容绘制。
|
||||
- DCL:Document Content Loaded,DOM 内容加载完毕。
|
||||
|
||||
再下面是 JS 计算消耗,用了一张火焰图,火焰图是性能分析的常用可视化工具。以下面这张图为例:
|
||||
|
||||
<img width=350 src="https://img.alicdn.com/tfs/TB1JecIDrr1gK0jSZFDXXb9yVXa-1404-616.png">
|
||||
|
||||
看火焰图首先看跨度最长的函数,也就是最长的那条线,这是最耗时的部分,从左到右是浏览器脚本的调用顺序,从上到下是函数嵌套的顺序。
|
||||
|
||||
我们可以看到鼠标位置的 34 这个函数虽然长,但并不是性能瓶颈,因为下面执行的 n 函数长度和它一样,表示 34 函数的性能几乎无损耗,其性能由其调用的 n 函数决定。
|
||||
|
||||
我们可以利用这种方式一步步排查到叶子结点,找到对性能影响最大的元子函数。
|
||||
|
||||
### User Timing API
|
||||
|
||||
我们还可以利用 `performance.mark` 自定义性能检测节点:
|
||||
|
||||
```jsx
|
||||
// Record the time before running a task
|
||||
performance.mark("Movies:updateStart");
|
||||
// Do some work
|
||||
|
||||
// Record the time after running a task
|
||||
performance.mark("Movies:updateEnd");
|
||||
|
||||
// Measure the difference between the start and end of the task
|
||||
performance.measure("moviesRender", "Movies:updateStart", "Movies:updateEnd");
|
||||
```
|
||||
|
||||
这些节点可以在上面介绍的 Performance 面板中展示出来用于自定义分析。
|
||||
|
||||
## 3 总结
|
||||
|
||||
利用 Performance 进行通用性能分析,利用 React Profiler 进行 React 定制性能分析,这两个结合在一起几乎可以完成任何性能检测。
|
||||
|
||||
一般来说,首先应该用 React Profiler 进行 React 层面的问题筛查,这样更直观,更容易定位问题。如果某些问题跳出了 React 框架范围,或者不再能以组件粒度进行度量,我们可以回到 Performance 面板进行通用性能分析。
|
||||
|
||||
> 讨论地址是:[精读《React 性能调试》 · Issue #247 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/247)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
Reference in New Issue
Block a user