https://developer.chrome.com/docs/devtools/performance/selector-stats?hl=zh-cn
在 Chrome Performance 面板里,想看 Layout(重排 / 回流) 非常直观,我给你用最简单的方式讲清楚怎么看、怎么定位。
一、先打开 Performance
- F12 → Performance
- 点左上角 ● 录制
- 刷新页面 / 操作页面
- 点 ■ 停止,等待分析结果出来
二、怎么识别 Layout
主要看这 3 个地方:
1. 主面板颜色条(最直观)
- 蓝色条:Loading(加载、解析 HTML/CSS)
- 黄色条:Scripting(JS 执行)
- 紫色条:Rendering → 这里面就包含 Layout
- 绿色条:Painting(重绘)
- 灰色条:System / Idle
只要看到 紫色条特别长、特别多、特别碎,就是 Layout 太多、性能差。
2. 看 Main 线程任务里的标签
点开 Main 线程的火焰图:
- 看到 Layout 字样 → 就是重排
- 看到 Forced Synchronous Layout → 强制同步布局(最伤性能)
- 看到 Recalculate Style → 重新计算样式(一般会跟着 Layout)
3. 看 Summary 统计
底部 Summary 面板:
- Rendering 时间占比高 → Layout / 样式计算多
三、重点:怎么看 "哪个 JS 导致了 Layout"
- 找到紫色的 Layout 任务
- 点开右边 Call Stack(调用栈)
- 栈顶就是触发 Layout 的 JS 代码位置
- 点文件名,直接跳到源码
常见触发:
- 读写 offsetWidth /clientHeight/getBoundingClientRect
- 频繁改 style(width/height/top/left)
- 频繁增删 DOM
- 窗口 resize / 滚动
四、Forced Synchronous Layout(强制同步布局)
这是性能杀手,表现为:
- 火焰图里出现 密集小紫条
- 任务名标红:Forced Synchronous Layout
原因:JS 一边读布局属性 ,一边改样式,浏览器被迫不停重排。
五、你关心 ECharts 多图表场景怎么看
多 ECharts 刷新时,你重点看:
- 是不是一堆 密集紫色 Layout 条
- 是不是有 Forced Synchronous Layout
- ECharts 容器是否频繁触发尺寸计算(resize)
如果用了 transform: translateZ(0) 优化后:
- 紫色 Layout 条明显变少
- 绿色 Paint 也会减少
- 主要剩下灰色合成(Composite)
六、最简单判断标准
- 紫色条少 → Layout 少 → 流畅
- 紫色条多且碎 → Layout 爆炸 → 卡顿
- 出现红色 Forced Synchronous Layout → 必须优化
| 操作类型 | 触发的完整流程 | 性能开销 | 核心定义 | 示例属性/操作 |
|---|---|---|---|---|
| 重排 (Reflow / 回流) | Layout + Paint + Composite | ⚠️ 最高 | 重新计算元素的几何属性(位置、大小),会触发后续所有渲染阶段,影响范围可覆盖整个页面 | 宽高(width/height)、边距(margin/padding)、位置(top/left)、display(none/block)、字体(font-size)、添加/删除Dom元素,改变窗口大小 |
| 重绘 (Repaint) | Paint + Composite | ⚠️ 中等 | 仅重新绘制元素的外观属性,不改变几何位置,跳过布局计算 | 颜色(color/background-color)、可见性(visibility)、透明度(opacity, 不提升合成层时)、边框样式(border-style) |
| 合成 (Composite) | Composite only | ✅ 最低 | 仅在 GPU 层合并图层,不触发布局和绘制,是性能最优的操作 | transform、opacity(提升合成层后)、filter(部分) |
仅触发合成(零开销,优先使用)
通过 GPU 硬件加速,仅在合成层操作,不触发重排 / 重绘:
transform(平移、缩放、旋转等,浏览器默认提升为独立合成层)opacity(元素被提升为独立合成层后,修改仅触发合成)- 部分
filter属性(如blur,需浏览器支持合成层优化) will-change:提前告知浏览器元素将发生变化,主动提升合成层
提升合成层 :用will-change、transform: translateZ(0)主动提升元素为独立图层,减少重绘范围
transform的优势:完全在合成层执行,不触发重排 / 重绘,是动画性能最优方案
合成层由 GPU 管理,过多独立图层会增加内存开销,需合理控制图层数量
合浏览器渲染原理和 ECharts 特性,我们可以通过主动提升合成层 +合理使用 GPU 加速属性,大幅降低多图表页面刷新 / 渲染时的重排、重绘开销。
一、核心原理回顾
ECharts 的canvas渲染本质上是绘制在 DOM 元素上的位图,默认情况下,这些 canvas 会和页面其他元素共用合成层,每次刷新 / 重绘都会触发全页面的 Paint+Composite。通过以下方式,我们可以让图表容器独立为合成层,让后续的动画 / 更新仅触发 GPU 合成,不阻塞主线程:
transform:强制提升为独立合成层(浏览器默认优化)opacity:配合合成层,修改仅触发 GPU 合成filter:部分滤镜(如blur)可在 GPU 层执行,不触发布局重排will-change:提前告知浏览器元素将发生变化,主动提升合成层
二、实战优化方案(含代码)
1. 图表容器:强制提升为独立合成层
给每个 ECharts 容器添加合成层触发样式,让浏览器为其分配独立 GPU 图层,避免重绘污染其他元素。
/* 方案1:使用transform主动提升合成层(兼容性最好) */
.echarts-container {
/* translateZ(0) 强制开启GPU硬件加速,提升为独立合成层 */
transform: translateZ(0);
/* 优化渲染性能,避免图像抖动 */
backface-visibility: hidden;
perspective: 1000;
}
/* 方案2:will-change 提前告知浏览器(适合已知会频繁更新的图表) */
.echarts-container {
will-change: transform, opacity;
}
<!-- 每个图表容器都应用该样式 -->
<div class="echarts-container" id="chart1"></div>
<div class="echarts-container" id="chart2"></div>
2. 页面刷新 / 初始化时的性能优化
(1)批量初始化图表,减少重排次数
多图表同时初始化会导致浏览器多次重排,通过requestAnimationFrame批量执行初始化,合并重排:
// 错误示范:逐个初始化,触发多次重排
const chart1 = echarts.init(document.getElementById('chart1'));
const chart2 = echarts.init(document.getElementById('chart2'));
// 正确示范:批量初始化,合并重排
requestAnimationFrame(() => {
const charts = [];
// 所有图表DOM节点
const chartContainers = document.querySelectorAll('.echarts-container');
chartContainers.forEach((dom, index) => {
const chart = echarts.init(dom);
// 配置option...
charts.push(chart);
});
// 保存实例,后续更新使用
window.charts = charts;
});
(2)避免图表容器尺寸重排
图表容器尺寸变化会触发重排,提前固定容器尺寸或避免动态修改宽高:
.echarts-container {
width: 100%;
height: 300px; /* 固定高度,避免JS动态修改height */
transform: translateZ(0);
}
如果需要响应式适配,优先使用 CSS transform: scale() 缩放容器,而非修改width/height:
.echarts-wrapper {
transform: scale(0.8); /* 仅GPU合成,不触发布局重排 */
transform-origin: top left;
}
3. 图表更新 / 动画时:仅触发 GPU 合成
(1)数据更新优化:避免全量重绘
使用 ECharts 的增量更新 API,配合合成层,让更新仅在 GPU 层完成:
// 错误示范:setOption 全量更新,触发重绘+重排
chart.setOption(newOption);
// 正确示范:仅更新变化的数据,减少重绘范围
function updateChart(chart, newData) {
// 仅更新series数据,不修改其他配置
chart.setOption({
series: [{
data: newData,
animationDurationUpdate: 0 // 关闭不必要的更新动画,减少主线程开销
}]
});
}
(2)使用 opacity 实现淡入淡出动画(GPU 合成)
修改合成层上的opacity仅触发 GPU 合成,性能远优于修改visibility/display:
.echarts-container {
transform: translateZ(0);
transition: opacity 0.3s ease; /* GPU层执行过渡,不阻塞主线程 */
}
// 显示/隐藏图表,仅修改opacity,不触发重排/重绘
function toggleChart(chartDom, show) {
chartDom.style.opacity = show ? 1 : 0;
}
(3)filter 属性的 GPU 优化使用
部分filter滤镜(如blur)可在 GPU 层执行,适合图表加载时的占位效果:
.echarts-loading {
transform: translateZ(0);
filter: blur(2px); /* GPU层执行模糊,不触发布局重排 */
opacity: 0.7;
}
三、进阶优化:合成层管理与避坑
1. 避免合成层爆炸
- 不要给所有元素都加
transform: translateZ(0),过多合成层会增加 GPU 内存开销 - 仅给频繁更新 / 动画的图表容器提升合成层,静态图表无需额外处理
2. 合成层优化的验证方法
使用 Chrome DevTools 查看合成层:
- 打开控制台 →
More tools→Layers - 查看每个图表容器是否被标记为独立合成层(
Composited Layers) - 检查重绘区域:使用
Rendering面板的Paint flashing,确认图表更新时仅自身区域重绘
3. 兼容性说明
transform: translateZ(0)兼容性:所有现代浏览器均支持,移动端也兼容will-change:Chrome/FF 支持,旧版浏览器会忽略,不影响基础功能filter:部分浏览器对filter的 GPU 加速支持有限,优先用于非关键路径
四、额外的 ECharts 性能优化(配合合成层效果翻倍)
-
渲染器选择 :大数据量场景优先使用
renderer: 'canvas',避免 SVG 生成大量 DOM 节点const chart = echarts.init(dom, null, { renderer: 'canvas' }); -
数据降采样 :使用
sampling减少渲染数据点,降低 canvas 绘制开销option = { series: [{ type: 'line', data: largeData, sampling: 'lttb' // 大数据采样优化,保留关键拐点 }] }; -
关闭非必要动画 :禁用初始渲染和更新动画,减少主线程计算
option = { animation: false, animationDurationUpdate: 0, tooltip: { show: false } // 非必要交互组件可关闭 };
五、完整示例:多图表页面优化模板
<style>
.echarts-container {
width: 100%;
height: 300px;
margin: 10px 0;
/* 合成层优化 */
transform: translateZ(0);
will-change: opacity;
backface-visibility: hidden;
}
</style>
<div class="echarts-container" id="chart1"></div>
<div class="echarts-container" id="chart2"></div>
<script>
// 批量初始化图表,合并重排
requestAnimationFrame(() => {
const chartContainers = document.querySelectorAll('.echarts-container');
const charts = [];
chartContainers.forEach((dom, index) => {
const chart = echarts.init(dom, null, { renderer: 'canvas' });
chart.setOption({
xAxis: { type: 'category', data: ['Mon', 'Tue', 'Wed'] },
yAxis: { type: 'value' },
series: [{ type: 'line', data: [120, 200, 150], sampling: 'lttb' }],
animation: false,
animationDurationUpdate: 0
});
charts.push(chart);
});
// 后续更新时仅修改数据,不触发重排
window.updateCharts = (newDataList) => {
requestAnimationFrame(() => {
charts.forEach((chart, index) => {
chart.setOption({ series: [{ data: newDataList[index] }] });
});
});
};
});
</script>
💡 关键总结:合成层优化的核心是让图表容器成为独立 GPU 图层,后续的更新 / 动画仅触发合成,不阻塞主线程;同时配合 ECharts 自身的渲染优化,才能实现真正的流畅体验。
_01. 代码分离
1代码分离
默认情况下,所有的 JS 代码,包括业务代码、三方依赖以及 Webpack 所依赖的模块化代码都会被打包进入一个 bundle 文件中。当访问这个页面时,首先会下载这个页面的 HTML,然后进行解析,当解析到 script 标签时就会下载该标签 src 所引用的资源,由于所有的代码都在一个 bundle 文件中,所以该文件势必会非常大,就会造成白屏时间过长,严重影响首页的加载速度

解决单一 bundle 文件过大的方式就可以通过代码分离,使用代码分离可以按需加载或者并行加载这些文件,分离方式通常有:
- 动态导入:使用
import()这种方式导入 - 多入口起点:使用 entry 配置手动分离代码
- 自定义分包:Entry Dependencies 或者 SplitChunksPlugin 去重和分离代码
1.1. 多入口依赖
通过配置对象形式的 entry,实现多入口。此时,对应的 output.filename 配置项中 filename 需要使用[]的形式来保证每个入口对应一个出口
module.export = {
entry: {
main: "./src/main.js",
index: "./src/index.js"
},
output: {
filename: "[name].bundle.js"",
path: reslove(__dirname, "./dist"),
}
}
使用多入口的弊端:
如果不同入口的文件依赖了相同的库或者工具函数,这些内容将会被各自打包(重复),解决方法:
-
通过额外配置 shared 属性表明共享的模块
-
将每个入口变为对象形式,并且增加 dependOn 选项
module.exports = {
entry: {
index: {
import: "./src/index.js",
dependOn: "shared"
},
main: {
import: "./src/main.js",
dependOn: "shared"
},
shared: ["axios"]
}
}
最终生成的打包结果中 index 中将会引入三个 bundle:

1.2. 动态导入
动态导入允许在代码执行过程中按需加载特定的模块,只有当模块被真正用到时,相关的代码才会被加载和执行。有两种动态导入的方式:
- 使用
import()函数语法(导出的内容通过 import.then 中res.default()来获取) - 使用 Webpack 已弃用的 require.ensure
index.js文件中通过import()动态引入 JS 文件
const button = document.createElement('button');
document.body.appendChild(button);
button.onclick = () => {
import('./router/about');
}
动态导入的模块会被单独打包到一个文件中,且 HTML 文件中是不会引入 src_router_about_js_bundle.js 的
⚡包名称:

对于动态导入文件最终打包出来的名称,默认使用 filename 中设置名称规则。如果想对动态单独生成的包文件进行命名,可以在 output 中配置额外的 chunkFilename 来进行定义
module.export = {
output: {
clean: true,
path: resolve(__dirname, "./dist"),
filename: "[name]-bundle.js",
// 只针对分包的文件命名,默认情况下获取到的 id 与 name 是一致的
chunkFilename: "[id]_[name]_chunk.js"
}
}

如果想要自定义名称,则需要使用魔法注释/* webpackChunkName: '' */进行命名:
button.onclick = () => {
import(/* webpackChunkName: 'About' */ './router/about');
}

1.3. SplitChunks
第三种分包模式是 SplitChunks,底层使用了 SplitChunksPlugin 实现,在 Webpack5 中该插件已默认安装
默认情况下 SplitChunksPlugin 的默认值 async 只针对import()进行分包。但所使用的第三方库,例如 axios 即便导入和使用了,其库本身也会被放入主包