DrillLab
第 11 / 25 节LESSON 11 / 25约 26 分钟~26 min

性能与新特性 · 八问8 questions on performance and new features

性能优化、写样式的几种方式、React 18 新变化、lazy、最佳实践、StrictMode、错误边界、Router。Performance work, the ways to write styles, what is new in React 18, lazy, best practices, StrictMode, error boundaries, Router.

面试 · 第 5 部分Interview · Part 5
这一页有什么On this page9
学完这节你会After this lesson you can
  • 按「先测量再优化」的顺序列出 React 性能优化手段List the ways to make React faster, in the right order: measure first, then optimise
  • 说清 React 18 的自动批处理和并发特性带来的实际差别Explain the real difference made by automatic batching and the concurrent features in React 18
  • 解释 StrictMode 为什么故意渲染两次Explain why StrictMode renders twice on purpose
  • 说明错误边界能抓什么、不能抓什么Say what an error boundary catches and what it does not catch
这在考试里考什么What the exam does with this

性能优化那道是开放题,最能看出你有没有真调过 —— 先说「用 Profiler 找出问题」比直接列 API 高一个档。React 18 和 StrictMode 那两道会问到「为什么」,答得出并发和纯函数就说明理解了设计动机。The performance question is open-ended, and it shows better than any other whether you have really tuned an app. Saying "first I use the Profiler to find the problem" ranks a level above listing APIs. For React 18 and StrictMode the interviewer asks why, and answering with concurrent rendering and pure functions shows you understand what the design is for.

§01

怎么优化 React 性能How do you make a React app faster?

#343 How could you improve performance in React

先说这一句,再列手段:「先用 React DevTools Profiler 找出到底哪个组件渲染慢、渲染了多少次, 再决定动哪。」上来就背 useMemo会显得像背题。

手段分三类:

① 少渲染

  • React.memo +useMemo +useCallback三件套配套用(#346)
  • state 下移—— 把频繁变的 state 放到真正需要它的那个小组件里, 别提到顶层带着整棵树重渲染。这招常常比加 memo 有效得多。
  • children 组合—— 父组件重渲染时, 作为 prop 传进来的 children不会重建
  • 拆分 Context —— 一个 context 里放太多东西, 改任何一项所有消费者都重渲染。

② 少下载

  • 代码分割——React.lazy +Suspense,按路由切(#347)
  • 按需引入第三方库,别import _ from "lodash"
  • 用 bundle analyzer 看谁占体积

③ 少算 / 少画

  • 长列表虚拟化—— 只渲染视口内的几十行。一万行的列表,这一条比其他所有优化加起来都有用。
  • 列表 key 用稳定 id(#330)
  • 输入防抖、搜索节流
  • React 18 的 useTransition /useDeferredValue—— 让重活不挡住输入

会追问:「怎么知道有没有多余渲染?」—— Profiler 的「Highlight updates」 或者 <Profiler onRender>; 以及注意 StrictMode 下开发模式会渲染两次, 别把它当成 bug。

Open with this sentence, then list the techniques: “First I use the React DevTools Profiler to find which component is slow and how many times it renders, then I decide what to touch.” Reciting useMemo straight away sounds like you memorised an answer sheet.

Three families of technique:

① Render less

  • React.memo + useMemo + useCallback used as a set (#346)
  • Push state down — put frequently changing state in the small component that actually needs it instead of lifting it to the top and re-rendering the whole tree. This often helps far more than adding memo.
  • Compose with children — when the parent re-renders, children passed in as a prop are not rebuilt.
  • Split your contexts — put too much in one context and changing any field re-renders every consumer.

② Download less

  • Code splittingReact.lazy + Suspense, split per route (#347)
  • Import third-party libraries piecemeal, not import _ from "lodash"
  • Run a bundle analyzer to see who is taking up the space

③ Compute less, paint less

  • Virtualise long lists — render only the few dozen rows in the viewport. On a ten-thousand-row list this beats every other optimisation put together.
  • Use stable ids as list keys (#330)
  • Debounce input, throttle search
  • React 18’s useTransition / useDeferredValue — keep the heavy work from blocking typing

Follow-up: “How do you know there are wasted renders?” — the Profiler’s “Highlight updates”, or <Profiler onRender>; and remember StrictMode renders twice in development, so do not mistake that for a bug.

§02

React 里怎么写样式How do you write styles in React?

#342 How to use styles in React

一句话:五种, 各有明确的取舍。

方式好处代价
普通 CSS / SCSS 文件零成本、能用全部 CSS 特性类名全局,会冲突
CSS Modules类名自动加哈希,天然隔离动态样式要配 CSS 变量
行内 style动态值最直接没有伪类、媒体查询、动画; 每次渲染新对象
CSS-in-JS(styled-components)能用 props 决定样式,作用域天然隔离运行时开销,SSR 要额外配置
原子化(Tailwind)不用起类名,产物体积可控JSX 里类名很长,团队要统一约定

「动态样式」的推荐做法—— 这是加分点:用 CSS 变量而不是行内 style。 把变量写在行内, 真正的样式规则还在 CSS 文件里 —— 这样既能动态,又保留伪类和媒体查询。本站的深色模式就是这么做的(切 data-theme 属性, CSS 变量整套换)。

会追问:「行内 style 为什么影响性能?」—— 每次渲染都创建新对象, 会破坏子组件的 memo; 而且它不能被浏览器按规则缓存。要用就 useMemo 稳住。

In one line: five ways, each with a clear trade-off.

ApproachUpsideCost
Plain CSS / SCSS filesFree, and every CSS feature is availableClass names are global, so they collide
CSS ModulesHashed class names, isolated by defaultDynamic styles need CSS variables
Inline styleThe most direct way to use a dynamic valueNo pseudo-classes, media queries or animations; a new object every render
CSS-in-JS (styled-components)props can drive the styles, and scoping needs no extra workRuntime cost, and SSR needs extra setup
Atomic (Tailwind)No naming, and the output size stays under controlVery long class strings in JSX, and the team needs conventions

The recommended way to do dynamic styles — this is the bonus point: use a CSS variable, not an inline style. Put only the variable inline and leave the actual rule in the CSS file — you get the dynamic value and keep pseudo-classes and media queries. That is how this site’s dark mode works (flip the data-theme attribute and the whole set of CSS variables swaps).

Follow-up: “Why do inline styles hurt performance?” — every render creates a new object, which breaks memo on the child, and the browser cannot cache it as a rule. If you must use one, stabilise it with useMemo.

JSX动态样式的正确做法示意Illustrative
1// 推荐:行内只放变量,规则留在 CSS 里
2<div className="bar" style={{ "--pct": `${percent}%` }} />
3
4/* CSS 里 */
5.bar::after { width: var(--pct); } /* 伪类照样能用 */
6@media (max-width: 480px) { .bar { height: 4px; } }
7
8// ✗ 行内写全套:没法写伪类和媒体查询,还每次新对象
9<div style={{ width: `${percent}%`, background: "#2b6" }} />
1// Recommended: only the variable goes inline, the rules stay in CSS
2<div className="bar" style={{ "--pct": `${percent}%` }} />
3
4/* In the CSS */
5.bar::after { width: var(--pct); } /* pseudo-classes still work */
6@media (max-width: 480px) { .bar { height: 4px; } }
7
8// ✗ everything inline: no pseudo-classes, no media queries, and a new object each time
9<div style={{ width: `${percent}%`, background: "#2b6" }} />
§03

React 18 有哪些新变化What is new in React 18?

#344 What are the new changes in react 18

一句话:核心是并发渲染(concurrent rendering)—— React 可以中断、暂停、恢复一次渲染, 好让高优先级的更新先跑。

五个具体变化(挑三四个说清就够):

  • 自动批处理(automatic batching)—— React 17 只在 React 事件回调里批处理,setTimeout、 Promise、原生事件里的多次setState 会各触发一次渲染。18 里全都批处理了。这是最容易被观察到的变化。
  • useTransition—— 把一个更新标记为「不着急」。 典型场景:输入框旁边有个很重的搜索结果列表,输入保持流畅,列表慢慢跟上
  • useDeferredValue—— 同一个目的, 但是从「值」的角度:给我一个滞后版本的值。
  • 新的 root API——createRoot 取代ReactDOM.render不换它就用不上任何并发特性
  • Suspense 支持 SSR—— 流式渲染、选择性 hydration。
    另外 useId(服务端客户端一致的 id)、useSyncExternalStore(给状态库用的)。

会追问:「StrictMode 在 18 里有什么变化?」——会额外「挂载 → 卸载 → 再挂载」一次, 所以 effect 的清理函数会被执行。这是为了提前暴露「没写清理函数」的组件(见 #332),为将来的 Offscreen 特性做准备。
「升级要注意什么?」—— 换 createRoot; 自动批处理可能让依赖「每次 setState 都立刻渲染」 的老代码行为改变 (必要时用 flushSync 逃出批处理)。

In one line: the core is concurrent rendering — React can interrupt, pause and resume a render so a higher-priority update goes first.

Five concrete changes — covering three or four properly is enough:

  • Automatic batching — React 17 only batched inside React event handlers, so several setState calls in a setTimeout, a Promise or a native event each triggered their own render. 18 batches all of them. This is the change you notice most easily.
  • useTransition — mark an update as “not urgent”. The classic case is an input next to a heavy list of search results: typing stays smooth and the list catches up.
  • useDeferredValue — same goal, from the value side: give me a version of this value that lags behind.
  • The new root APIcreateRoot replaces ReactDOM.render, and until you switch you get none of the concurrent features.
  • Suspense works on the server — streaming and selective hydration.
    Plus useId (ids that match between server and client) and useSyncExternalStore (for state libraries).

Follow-up: “What changed for StrictMode in 18?” — it now does an extra mount, unmount, mount, so effect cleanup functions actually run. The point is to expose components that forgot to write cleanup (see #332) and to prepare for the future Offscreen feature.
“What do you watch out for when upgrading?” — switch to createRoot; automatic batching can change the behaviour of old code that assumed “every setState renders right away” (use flushSync to escape a batch when you truly need to).

JSX两个最能感知的变化示意Illustrative
1// React 17:这里会渲染两次
2setTimeout(() => {
3 setA(1);
4 setB(2);
5}, 0);
6// React 18:只渲染一次(自动批处理)
7
8// useTransition:输入不卡,列表慢慢跟
9const [isPending, startTransition] = useTransition();
10
11function onChange(e) {
12 setQuery(e.target.value); // 紧急:输入框马上响应
13 startTransition(() => setList(filter(e.target.value))); // 不急
14}
15{isPending && <span>更新中…</span>}
1// React 17: this renders twice
2setTimeout(() => {
3 setA(1);
4 setB(2);
5}, 0);
6// React 18: renders once (automatic batching)
7
8// useTransition: the input stays responsive and the list catches up
9const [isPending, startTransition] = useTransition();
10
11function onChange(e) {
12 setQuery(e.target.value); // urgent: the input responds at once
13 startTransition(() => setList(filter(e.target.value))); // not urgent
14}
15{isPending && <span>Updating</span>}
§04

React.lazy 是干什么的What does React.lazy do?

#347 What is React lazy function

一句话:让组件按需加载—— 打包时被切成单独的 chunk, 真正渲染到它的时候才下载。 必须配 Suspense 给个占位。

为什么需要:SPA 默认把全站打成一个包, 首屏要下载所有页面的代码 (见 #321)。按路由切分是收益最大的一刀 —— 用户打开首页不该下载「设置页」的代码。

四个注意点:

  • lazy 的参数必须返回一个default 导出的 Promise —— 所以配的是 default export; 具名导出要自己包一层。
  • 动态 import()的路径不能是完全动态的变量, 打包工具需要在编译期能分析出来。
  • 加载失败要有兜底—— 网络断了 chunk 拉不下来, 要用错误边界包住(见 #333)。这一点很多人不提,是加分项。
  • 别切太碎 —— 每个 chunk 都是一次请求。

会追问:「怎么避免切换页面时闪一下 loading?」——预加载:鼠标悬停在链接上时 就调一次那个 import()(模块会被缓存)。 或者用 React 18 的useTransition让旧页面留在屏幕上直到新页面就绪。

In one line: it makes a component load on demand — the bundler cuts it into its own chunk, and the chunk is fetched only when you actually render it. It needs a Suspense around it to supply a placeholder.

Why you need it: by default an SPA is one bundle, so the first screen downloads the code for every page (see #321). Splitting per route is the highest-value cut — opening the home page should not download the settings page.

Four things to watch:

  • What you pass to lazy must return a Promise with a default export — so it pairs with default export; a named export needs a small wrapper.
  • The path in a dynamic import() cannot be a fully dynamic variable — the bundler has to be able to analyse it at build time.
  • Handle the failure case — if the network drops the chunk never arrives, so wrap it in an error boundary (see #333). Most people leave this out, so it is a bonus point.
  • Do not split too finely — every chunk is another request.

Follow-up: “How do you avoid a loading flash when switching pages?” — preload: call that import() once when the mouse hovers the link (the module gets cached). Or use React 18’s useTransition to keep the old page on screen until the new one is ready.

JSXlazy 的完整用法(含失败兜底)示意Illustrative
1const Settings = lazy(() => import("./pages/Settings"));
2
3<ErrorBoundary fallback={<p>加载失败,请刷新</p>}>
4 <Suspense fallback={<Spinner />}>
5 <Settings />
6 </Suspense>
7</ErrorBoundary>
8
9// 预加载:悬停时就开始下载
10const preload = () => import("./pages/Settings");
11<Link to="/settings" onMouseEnter={preload}>设置</Link>
1const Settings = lazy(() => import("./pages/Settings"));
2
3<ErrorBoundary fallback={<p>Loading failed, please refresh</p>}>
4 <Suspense fallback={<Spinner />}>
5 <Settings />
6 </Suspense>
7</ErrorBoundary>
8
9// Preloading: start downloading on hover
10const preload = () => import("./pages/Settings");
11<Link to="/settings" onMouseEnter={preload}>Settings</Link>
§05

什么是 StrictModeWhat is StrictMode?

#332 What is React strict mode

一句话:一个只在开发模式生效的检查工具组件, 通过故意多做一次来暴露不安全的写法。生产构建里它什么都不做。

它做三件事:

  • 渲染函数调用两次—— 暴露渲染过程中的副作用 (改了外部变量、直接 mutate props)。因为 render 阶段可能被中断重跑 (见 #353),所以它必须是纯的。
  • React 18 起:effect 会 「挂载 → 卸载 → 再挂载」——专门暴露「没写清理函数」的 effect。 如果你的组件挂两次就出问题 (重复订阅、定时器翻倍), 那就是真 bug。
  • 警告废弃 API 和过时的 ref 写法。

最重要的一句: 「双重渲染导致的问题」不是 StrictMode 的问题, 是你的代码的问题。很多人第一反应是「关掉它」—— 正确反应是修好副作用。

会追问:「日志被打印两次正常吗?」—— 开发模式下正常,因为渲染函数被调了两次; 生产不会。
「请求被发两次呢?」——这个要认真看: 说明你的 effect 没有正确处理清理 —— 虽然对幂等的 GET 无害, 但它同时提示你「竞态防护写了没有」。我们那道 fetch 变式题里ignore 标志 +abort 的写法, 正好让它在 StrictMode 下也表现正确。

In one line: a checking component that only does anything in development, and it exposes unsafe code by deliberately doing things twice. In a production build it does nothing.

It does three things:

  • Calls your render function twice — which surfaces side effects during render (mutating an outer variable, mutating props directly). Because the render phase can be interrupted and re-run (see #353), it has to be pure.
  • Since React 18: effects run mount, unmount, mountspecifically to expose effects with no cleanup function. If mounting twice breaks your component (a duplicated subscription, a doubled timer), that is a real bug.
  • Warns about deprecated APIs and legacy ref patterns.

The most important sentence: a problem caused by double rendering is not StrictMode’s problem, it is your code’s. Most people’s first instinct is to turn it off — the right instinct is to fix the side effect.

Follow-up: “Is it normal for my log to print twice?” — yes in development, because the render function ran twice; it will not in production.
“What about my request firing twice?” — take that one seriously: it means your effect is not cleaning up properly. Harmless for an idempotent GET, but it is also telling you to ask whether you guarded against races. The ignore flag plus abort pattern from our fetch variant is exactly what makes it behave correctly under StrictMode too.

§06

什么是错误边界,有什么用What is an error boundary, and what is it for?

#333 What are error boundaries and How are they useful

一句话:一个能捕获子树渲染错误的类组件, 出错时渲染兜底 UI,而不是让整个应用白屏

怎么写:实现 getDerivedStateFromError(用来切换到兜底 UI)和componentDidCatch(用来上报日志)。目前必须是 class 组件—— 没有对应的 hook。

抓不到什么(这才是考点):

  • 事件处理函数里的错误—— 因为那不在渲染过程中。 要自己 try/catch
  • 异步代码里的错误——setTimeout、 Promise 回调(和 #310 同一个道理)。
  • 服务端渲染
  • 错误边界自己抛的错。

放哪:粒度要合适。只在根节点放一个, 一个小组件挂了整页还是没了;推荐按「独立区块」放—— 每个路由页面、 每个可独立失败的挂件 (侧栏、图表、评论区)。 这样一块坏了其他还能用。

会追问:「怎么让用户能恢复?」—— 兜底 UI 里给一个「重试」按钮, 把边界的 state 重置 (或者换一个 key强制重建子树)。react-error-boundary这个库提供了 resetErrorBoundary

In one line: a class component that catches render errors in its subtree and renders a fallback UI instead of letting the whole app go blank.

How you write one: implement getDerivedStateFromError (to switch to the fallback) and componentDidCatch (to report the error). It still has to be a class component — there is no hook equivalent.

What it does not catch — this is the real question:

  • Errors inside event handlers — those do not happen during render. Use your own try/catch.
  • Errors in async codesetTimeout, Promise callbacks (the same reason as #310).
  • Server-side rendering.
  • Errors thrown by the boundary itself.

Where to put them: get the granularity right. One at the root only means a single broken widget still wipes out the page; put one around each independent block — each route, and each widget that can fail on its own (sidebar, chart, comments). Then one broken block leaves the rest usable.

Follow-up: “How does the user recover?” — give the fallback a “Retry” button that resets the boundary’s state (or changes a key to force the subtree to rebuild). The react-error-boundary library gives you resetErrorBoundary for this.

JSX错误边界示意Illustrative
1class ErrorBoundary extends React.Component {
2 state = { error: null };
3
4 static getDerivedStateFromError(error) {
5 return { error }; // 切到兜底 UI
6 }
7
8 componentDidCatch(error, info) {
9 report(error, info.componentStack); // 上报
10 }
11
12 render() {
13 if (this.state.error) {
14 return (
15 <div>
16 <p>这块出错了</p>
17 <button onClick={() => this.setState({ error: null })}>重试</button>
18 </div>
19 );
20 }
21 return this.props.children;
22 }
23}
24
25// 按区块放,而不是只在根节点放一个
26<ErrorBoundary><Sidebar /></ErrorBoundary>
27<ErrorBoundary><Chart /></ErrorBoundary>
1class ErrorBoundary extends React.Component {
2 state = { error: null };
3
4 static getDerivedStateFromError(error) {
5 return { error }; // switch to the fallback UI
6 }
7
8 componentDidCatch(error, info) {
9 report(error, info.componentStack); // report it
10 }
11
12 render() {
13 if (this.state.error) {
14 return (
15 <div>
16 <p>Something went wrong in this part</p>
17 <button onClick={() => this.setState({ error: null })}>Retry</button>
18 </div>
19 );
20 }
21 return this.props.children;
22 }
23}
24
25// Put one per region rather than a single one at the root
26<ErrorBoundary><Sidebar /></ErrorBoundary>
27<ErrorBoundary><Chart /></ErrorBoundary>
§07

React Router 的意义是什么What is the point of React Router?

#334 React router, What is the point of it

一句话:让 SPA有「URL」这个概念—— 把地址栏和当前渲染哪个组件对应起来, 不刷新页面就能切换。

它解决四件事:

  • URL ↔ 组件的映射
  • 前进后退能用—— 接管 history API
  • 深链接可分享—— 发给别人能直接打开那个页面
  • 嵌套路由—— 布局层和内容层分开 (Outlet

核心 API:BrowserRouterRoutes / RouteLink /NavLinkuseNavigateuseParamsuseSearchParamsOutlet

三个实践要点:

  • Link 不要用<a>—— 后者会真的刷新整页, SPA 的意义就没了。
  • BrowserRouter 需要服务端 把所有路径都回退到index.html, 否则刷新子路由会 404。 没法配服务器就用HashRouter这题很常问。
  • React.lazy按路由切代码(#347)。

会追问:「路由守卫怎么做?」—— React Router 没有内置守卫, 自己写一个包装组件: 没登录就 <Navigate to="/login" />

In one line: it gives an SPA the concept of a URL — it maps the address bar to whichever component is rendered, and switches between them without a page reload.

It solves four things:

  • Mapping URLs to components
  • Back and forward work — it takes over the history API
  • Deep links are shareable — send one to someone and it opens that page directly
  • Nested routes — the layout layer and the content layer stay separate (Outlet)

The core API: BrowserRouter, Routes / Route, Link / NavLink, useNavigate, useParams, useSearchParams, Outlet.

Three practical points:

  • Use Link, not <a> — an anchor really does reload the whole page, which throws away the point of an SPA.
  • BrowserRouter needs the server to fall back to index.html for every path, otherwise refreshing a nested route 404s. If you cannot configure the server, use HashRouter. This one comes up a lot.
  • Pair it with React.lazy to split code per route (#347).

Follow-up: “How do you do route guards?” — React Router has none built in; you write a wrapper component yourself: if the user is not signed in, render <Navigate to="/login" />.

§08

写 React 时你会注意哪些最佳实践Which best practices do you follow when writing React?

#348 When coding React, what are some best practices that you keep in mind

这是开放题,答「有取舍的清单」比列一堆规则好。按重要性给六条:

  • 不可变更新。永远造新对象/新数组, 不就地改 —— 否则 React 比引用发现不了变化,界面不更新。这是 React 里最常见的 bug 来源。
  • 能算出来的别放 state。派生数据当场算, 不要用 useEffect 同步两份数据 —— 同一个事实存两份必然会不一致。
  • state 放在「刚好够用」的那一层。提太高造成 drilling 和多余渲染, 提太低兄弟拿不到。
  • effect 里建立的东西一定要清理。定时器、监听器、订阅、在途请求。判别法:effect 里出现setInterval /addEventListener /subscribe /fetch,就一定要return
  • 列表 key 用稳定业务 id,不用 index。
  • 先测量再优化。别默认给所有东西套useMemo

再补两条工程上的:组件保持小而专一 (一个组件干一件事);用 TypeScript—— props 的形状是组件的契约, 写下来比靠记忆可靠。

如果面试官想听更具体的, 可以说: 「我会开着 eslint-plugin-react-hooks不用注释关掉exhaustive-deps 警告—— 它几乎每次都是对的, 想绕过它通常说明该重构了。」 这条很能体现实战经验。

This is an open question, and a list with trade-offs beats a pile of rules. Six, most important first:

  • Update immutably. Always build a new object or array, never edit in place — otherwise React compares references, sees nothing changed, and the UI does not update. This is the single most common source of bugs in React.
  • If you can compute it, do not store it in state. Derive it on the spot; do not use useEffect to keep two copies in sync — one fact stored twice will drift.
  • Keep state at the lowest level that works. Too high and you get prop drilling and wasted renders; too low and siblings cannot reach it.
  • Anything an effect sets up has to be torn down. Timers, listeners, subscriptions, in-flight requests. The test: if the effect contains setInterval, addEventListener, subscribe or fetch, it must return something.
  • Use stable business ids as list keys, not the index.
  • Measure before optimising. Do not wrap everything in useMemo by default.

Two more on the engineering side: keep components small and single-purpose (one component, one job); and use TypeScript — the shape of the props is the component’s contract, and writing it down beats remembering it.

If the interviewer wants something more specific, you can say: “I keep eslint-plugin-react-hooks on and I do not silence the exhaustive-deps warning with a comment — it is right almost every time, and wanting to get around it usually means the code needs restructuring.” That one really shows hands-on experience.

迁移Transfer

换一道题也能用Works on other problems too

考试不会原题重考。真正能带走的是「看到这种信号 → 伸手去拿这个解法」。The exam will not reuse the same question. What you take away is the reflex: see this signal, reach for that solution.

问性能优化Asked about performance work
先说用 Profiler 测量,再分「少渲染/少下载/少算」三类Start with measuring in the Profiler, then split the answer three ways: render less, download less, compute less
长列表卡A long list feels slow
虚拟化,收益远大于 memoRender only the visible rows; this helps far more than memo
频繁变的 state 拖累整棵树State that changes often slows down the whole tree
state 下移,别提到顶层Move that state down to the component that needs it instead of keeping it at the top
要动态样式You need styles that change at runtime
行内只放 CSS 变量,规则留在 CSS 文件Put only CSS variables inline and keep the rules in the CSS file
「setState 两次只渲染一次了」Two setState calls now cause only one render
React 18 自动批处理;要立即渲染用 flushSyncReact 18 batches them automatically; use flushSync if you need the render right away
「effect 跑了两次 / 日志打两遍」The effect runs twice, or a log appears twice
StrictMode 故意的,检查清理函数写了没StrictMode does that on purpose; check that you wrote the cleanup function
「一个小组件报错整页白屏」One small component throws and the whole page goes blank
按区块放错误边界Put an error boundary around each section of the page
「刷新子路由 404」Reloading a nested route gives a 404
服务端配 history fallback,或用 HashRouterConfigure a history fallback on the server, or use HashRouter
这节的要点What to take away
  1. 性能优化先用 Profiler 测量;三类手段是少渲染、少下载、少算,长列表虚拟化收益最大。Measure in the Profiler before you optimise. The three kinds of fix are render less, download less, compute less, and rendering only the visible rows of a long list pays off most.
  2. 动态样式用 CSS 变量而不是行内 style —— 保留伪类和媒体查询。For styles that change at runtime use a CSS variable rather than an inline style, so you keep pseudo-classes and media queries.
  3. React 18 核心是并发渲染;最易感知的是自动批处理,用不上并发特性通常是没换 createRoot。The core of React 18 is concurrent rendering. The change you notice first is automatic batching, and if the concurrent features do nothing for you it is usually because you did not switch to createRoot.
  4. React.lazy 要配 Suspense,还要配错误边界兜住 chunk 加载失败。React.lazy needs Suspense around it, and also an error boundary in case the chunk fails to load.
  5. StrictMode 只在开发生效,故意双渲染和双挂载来暴露不纯的渲染和缺失的清理函数。StrictMode runs in development only. It renders and mounts twice on purpose, to expose a render that is not pure and a missing cleanup function.
  6. 错误边界抓不到事件回调、异步代码、SSR 的错误;要按区块放而不是只放根节点。An error boundary does not catch errors in event handlers, in async code, or during SSR. Place one per section of the page rather than only at the root.
  7. Router 用 Link 不用 a;BrowserRouter 需要服务端 history fallback。With Router use Link, not a. BrowserRouter needs a history fallback on the server.
  8. 最佳实践六条:不可变更新、别存派生数据、state 放刚好够用的层、清理副作用、稳定 key、先测量再优化。Six best practices: update without changing the original, do not store what you can compute, keep state at the lowest level that works, clean up side effects, use stable keys, measure before you optimise.

接下来What next

  1. 接着看下一节Continue to the next lessonRedux 与 TypeScript · 六问6 questions on Redux and TypeScript
    下一节Next lesson
  2. 可选:再巩固一下Optional: reinforce it这一节的 8 道八股8 questions from this lesson
读完并且做过上面的练习了吗?Read it and worked through the exercises above?
上一节:Previous: Hooks 四问4 questions on Hooks