前端性能优化核心点
面试题:你是怎么做性能优化的?
核心四点:
- 打开速度怎么变快?—— 首屏加载优化
- 再次打开速度怎么变快?—— 缓存
- 操作怎么顺滑?—— 渲染优化
- 动画怎么流畅?—— 拆分长任务
首屏性能指标
如何衡量加载情况?
指标衡量:
- FP(First Paint 首次绘制)
- FCP(First Contentful Paint 首次内容绘制),FP 到 FCP 之间存在 SPA 应用的 JS 执行,太慢会导致白屏
- FMP(First Meanful Paint 首次有效绘制),主要内容呈现时间
- LCP(Largest Contentful Paint 最大内容绘制),加载最大内容块时间
其中 FP、FCP 可以用
Performance
工具检测FMP 可以使用
MutationObserver
采集
其他提升用户体验的指标:
- INP(Interaction to Next Paint)与网页进行的每次点按、点击或键盘互动的延迟时间。
- TTI(Time to Interaction 可交互时间)
- TBT(Total Blocking Time 总阻塞时间),从 FCP 到 TTI 的时间
- CLS(Commulative Layout Shift),布局偏移分数
- TTFB(Time to First Byte 首字节到达时间)请求发出后到接收到数据的时间
首屏加载优化
方案
- 优化图片:Webp、图片压缩、图片尺寸(在合适的容器中使用合适的图,如 1 倍图、2 倍图、3 倍图)
- 字体瘦身:字体瘦身主要是设计型产品,方案有字体子集化
字体子集化(Font subsetting)是指用了哪些字体,最后就只生成对应字的字体文件。
比如使用开源 fontmin 生成字体子集。
-
懒加载资源:图片懒加载、JS 异步加载
-
精简 CSS 和 JavaScript(打包构建阶段):
- 代码压缩:移除空格、注释和多余文字,减少文件大小
- 合并文件:将多个 CSS 和 JavaScript 文件合并为一个文件,减少 HTTP 请求次数
- Tree Shaking:移除未使用的代码,减少打包文件的体积
-
CDN(Content Deliver Network):将静态资源托管到 CDN 上,缩短资源
-
浏览器缓存:设置适当的缓存策略,使浏览器能够缓存常用的文件,减少重复加载
-
压缩文本资源:Gzip 或 Brotli 压缩
-
SSR、SSG
动画卡顿
为什么会卡顿?单线程
-
减少主线程阻塞
- 优化 JavaScript 执行,减少长任务(复杂的计算)
- 将耗时操作移至 Web Worker
-
GPU 加速
- 使用 CSS 属性(如
transform
和opacity
)触发 GPU 加速 - 避免使用
left
、top
等触发重排的属性
- 使用 CSS 属性(如
-
合适的帧率
- 确保动画运行在 60 帧/秒(FPS),通过
requestAnimationFrame
控制动画
- 确保动画运行在 60 帧/秒(FPS),通过
-
压缩动画帧渲染时间
- 减少每帧渲染的计算,避免阻塞绘制。
-
节流和防抖
- 优化滚动时间、窗口调整大小时间,减少不必要的频繁触发。
应用状态管理优化
React 状态管理
-
状态局部化,减少全局状态的依赖
- 避免使用全局状态(如 Redux 或 Context)管理所有数据
- 对于仅在某些组件的状态,可以使用撞见的
useState
或useReducer
-
优化 Context 性能,Context 的更新会重新渲染所有订阅的组件
- 比如拆分 Context,将不同的逻辑存储在多个 Context 中,降低重新渲染范围
-
使用高效的状态管理库
- 使用轻量、高性能的状态管理工具,比如 Zustand、Jotai,它们具备更细粒度的状态更新机制
import create from 'zustand';
const useStore = create(set => ({
count: 0,
increment: () => set(state => ({ count: state.count + 1})),
}))
const Counter = () => {
const count = useStore(state => state.count);
const increment = useStore(state => state.increment);
return <button onClick={incremnt}>Count: {count}</button>
} -
避免不必要的状态更新
- 使用不可变数据结构(如
immer
)管理状态,减少对数据的直接修改
import produce from 'immer';
const nextState = produce(baseState, draft => {
draft.value = newValue
}) - 使用不可变数据结构(如
使用 React Developer Tools 插件,开启「Highlight updates when components render」:
HIX.ai 的更新情况:
飞书 的更新情况:
相比之下,飞书的优化做得更好,更新的颗粒度更小。
Vue 状态管理
- 精简 Vuex 或 Pinia 的全局状态
- 将不需要全局共享的状态移至组件内部,减少全局状态更新的开销
- 示例:使用
reactive
管理局部状态,而不是在全局 store 中存储
- 模块化和按需加载
- 将 Vuex 或 Pinia 的状态模块化,按需加载,提高性能
- 避免多余的 Getter 重计算
- 将计算密集型的逻辑放入组件的
computed
或watch
中,而不是在 store 的 getter 中
- 将计算密集型的逻辑放入组件的
应用视图层更新优化
React 的视图更新优化
-
使用
React.memo
防止不必要的重新渲染- 对函数组件进行包裹,只有 props 变化使才重新渲染
const MyComponent = React.memo(({ data }) => {
return <div>{ data }</div>
}) -
useMemo
或useCallbck
的优化- 使用
useMemo
缓存复杂计算的结果,使用useCallback
缓存函数实例
const computedValue = useMemo(() => heavyComputation(data), [data]);
const handleClick = useCallback(() => doSomething(), []); - 使用
-
拆分组件
- 将页面拆分为更小的组件,只更新必要的部分,避免整体重新渲染
-
使用虚拟滚动
- 对于长列表循环,使用虚拟滚动技术(如 React-Virtualized 或 React-Window)只渲染可见区域的内容
-
适当使用批处理更新
- 确保多个状态变更可以批量处理,减少渲染次数
Vue 的视图更新优化
- 避免多余的响应式数据
- 只对需要响应式的数据使用
ref
或reactive
,静态数据不需要响应式
- 只对需要响应式的数据使用
- 使用
v-once
和v-memo
- 对于不需要更新的静态内容,可以使用
v-once
渲染一次
- 对于不需要更新的静态内容,可以使用
- 拆分组件和局部更新
- 将大组件拆分为多个子组件,使用
keep-alive
缓存不活跃的组件,减少重新渲染的开销
- 将大组件拆分为多个子组件,使用
- 避免
watch
的过度使用- 优化
watch
的逻辑,仅对必要的依赖进行监听,减少副作用执行
- 优化
- 使用虚拟 滚动
- 对长列表使用虚拟滚动库(如 vue-virtual-scroller)进行优化
事件和渲染细节优化
- 节流和防抖
- 事件绑定
- 在 Vue 中使用
.native
修饰符直接绑定 DOM 事件 - 在 React 中,避免在子组件上过多传递回调函数
- 在 Vue 中使用
- 避免不必要的 DOM 操作
- 减少直接操作 DOM 的次数,尽量通过框架的响应式机制处理更新
- 异步加载和懒加载
- 对于路由组件、图片等使用懒加载技术,降低首次加载压力
- 使用请求合并
- 在需要多次请求时,合并请求以减少多余的网络开销
参考
https://u19tul1sz9g.feishu.cn/docx/NdqqdIX4hoQiIsxR2fecr7ZLnsc?pwd=6&a25855