流数据渲染优化
· 3 min read
切入点
如何对流失数据优化,提高渲染性能。从以下两点切入:
懒加载渲染: 可视区外部的信息不用渲染,当滚动到可视区时再渲染。 注意,此时需要一个buff来缓冲渲染到页面的信息,防止给用户造成流暂停的假象。
增量式渲染: 已经渲染到页面上的数据不用重复渲染,只渲染新增的数据。
示例
Note: 打开控制台检查正在输出的元素
Full String:
Full String:
What is reconciliation?
reconciliation
The algorithm React uses to diff one tree with another to determine which parts need to be changed.
update
A change in the data used to render a React app. Usually the result of `setState`. Eventually results in a re-render.
The central idea of React's API is to think of updates as if they cause the entire app to re-render. This allows the developer to reason declaratively, rather than worry about how to efficiently transition the app from any particular state to another (A to B, B to C, C to A, and so on).
Actually re-rendering the entire app on each change only works for the most trivial apps; in a real-world app, it's prohibitively costly in terms of performance. React has optimizations which create the appearance of whole app re-rendering while maintaining great performance. The bulk of these optimizations are part of a process called reconciliation.
Reconciliation is the algorithm behind what is popularly understood as the "virtual DOM." A high-level description goes something like this: when you render a React application, a tree of nodes that describes the app is generated and saved in memory. This tree is then flushed to the rendering environment — for example, in the case of a browser application, it's translated to a set of DOM operations. When the app is updated (usually via setState), a new tree is generated. The new tree is diffed with the previous tree to compute which operations are needed to update the rendered app.
Although Fiber is a ground-up rewrite of the reconciler, the high-level algorithm described in the React docs will be largely the same. The key points are:
Different component types are assumed to generate substantially different trees. React will not attempt to diff them, but rather replace the old tree completely.
Diffing of lists is performed using keys. Keys should be "stable, predictable, and unique."
Reconciliation versus rendering
The DOM is just one of the rendering environments React can render to, the other major targets being native iOS and Android views via React Native. (This is why "virtual DOM" is a bit of a misnomer.)
The reason it can support so many targets is because React is designed so that reconciliation and rendering are separate phases. The reconciler does the work of computing which parts of a tree have changed; the renderer then uses that information to actually update the rendered app.
This separation means that React DOM and React Native can use their own renderers while sharing the same reconciler, provided by React core.
Fiber reimplements the reconciler. It is not principally concerned with rendering, though renderers will need to change to support (and take advantage of) the new architecture.
原理详解
懒加载渲染伪代码
let content = '';
let buffer = ''
function updateState(chunk) {
buffer = chunk;
if('in view'){
content += buffer;
buffer = ''
} else if("out of view") {
buffer += chunk
}
requestAnimationFrame(updateState)
}
for (const chunk of "hello world") {
updateState(chunk)
}
增量式更新核心代码
import { useCallback, useRef, useEffect } from "react";
import { sleep } from "aio-tool";
export enum PromiseState {
Resume = 'resume',
Suspense = 'suspense',
Cancel = 'cancel'
}
export default function useIncreasingRender({
onContinue,
}: {
onContinue?: (value: string) => void;
}) {
// Ref to control the consumer flow
const promiseRef = useRef<PromiseState | Function>(PromiseState.Cancel);
// Buffer for accumulating incoming characters
const remainRef = useRef<string>("");
// Store the requestAnimationFrame handle so it can be canceled when needed
const renderLoopRef = useRef<number | null>(null);
const updater = useCallback(async () => {
if (promiseRef.current === PromiseState.Resume) {
onContinue(remainRef.current);
remainRef.current = "";
} else if (promiseRef.current === PromiseState.Suspense) {
// Wait for an external signal to resume
await new Promise((resolve) => {
promiseRef.current = (node: any) => {
resolve(true);
};
});
}
// Schedule next iteration in the next animation frame
renderLoopRef.current = requestAnimationFrame(updater);
}, []);
const cancel = useCallback(() => {
if (renderLoopRef.current !== null) {
cancelAnimationFrame(renderLoopRef.current);
renderLoopRef.current = null;
}
remainRef.current = "";
promiseRef.current = PromiseState.Cancel;
}, []);
const start = useCallback(() => {
promiseRef.current = PromiseState.Resume;
updater().then();
}, []);
const consume = useCallback(async (value: string) => {
if (promiseRef.current === PromiseState.Cancel) return true;
remainRef.current += value;
await sleep(0);
}, []);
useEffect(() => {
return cancel;
}, [])
return {
start,
cancel,
consume,
promiseRef,
remainRef,
};
}