296 lines
7.2 KiB
Markdown
296 lines
7.2 KiB
Markdown
|
|
# ArgonRenderOptimizer 使用指南
|
|||
|
|
|
|||
|
|
## 概述
|
|||
|
|
|
|||
|
|
`ArgonRenderOptimizer` 类用于优化 DOM 操作性能,通过批量读写避免布局抖动(Layout Thrashing),并提供 GPU 加速管理功能。
|
|||
|
|
|
|||
|
|
## 核心功能
|
|||
|
|
|
|||
|
|
### 1. 批量读写 DOM
|
|||
|
|
|
|||
|
|
**问题:** 频繁交替读写 DOM 会导致浏览器多次重排(reflow),严重影响性能。
|
|||
|
|
|
|||
|
|
**解决方案:** 使用 `read()` 和 `write()` 方法将操作加入队列,在下一帧统一执行(先读后写)。
|
|||
|
|
|
|||
|
|
### 2. GPU 加速管理
|
|||
|
|
|
|||
|
|
**功能:** 通过 `will-change` 属性提示浏览器创建合成层,利用 GPU 加速动画。
|
|||
|
|
|
|||
|
|
## 使用示例
|
|||
|
|
|
|||
|
|
### 基础用法
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
// 创建实例
|
|||
|
|
const optimizer = new ArgonRenderOptimizer();
|
|||
|
|
|
|||
|
|
// 批量读取 DOM 属性
|
|||
|
|
let scrollTop, windowHeight;
|
|||
|
|
|
|||
|
|
optimizer.read(() => {
|
|||
|
|
scrollTop = document.documentElement.scrollTop;
|
|||
|
|
windowHeight = window.innerHeight;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 批量写入 DOM
|
|||
|
|
optimizer.write(() => {
|
|||
|
|
const toolbar = document.querySelector('.navbar');
|
|||
|
|
if (toolbar) {
|
|||
|
|
toolbar.style.opacity = scrollTop > 100 ? '1' : '0.8';
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 滚动事件优化
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
// 错误示例:频繁交替读写导致布局抖动
|
|||
|
|
window.addEventListener('scroll', () => {
|
|||
|
|
const scrollTop = document.documentElement.scrollTop; // 读
|
|||
|
|
toolbar.style.opacity = scrollTop > 100 ? '1' : '0.8'; // 写
|
|||
|
|
|
|||
|
|
const windowHeight = window.innerHeight; // 读
|
|||
|
|
sidebar.style.height = windowHeight + 'px'; // 写
|
|||
|
|
// 每次滚动都会触发多次重排!
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 正确示例:使用 ArgonRenderOptimizer
|
|||
|
|
window.addEventListener('scroll', () => {
|
|||
|
|
let scrollTop, windowHeight;
|
|||
|
|
|
|||
|
|
// 批量读取
|
|||
|
|
optimizer.read(() => {
|
|||
|
|
scrollTop = document.documentElement.scrollTop;
|
|||
|
|
windowHeight = window.innerHeight;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 批量写入
|
|||
|
|
optimizer.write(() => {
|
|||
|
|
toolbar.style.opacity = scrollTop > 100 ? '1' : '0.8';
|
|||
|
|
sidebar.style.height = windowHeight + 'px';
|
|||
|
|
});
|
|||
|
|
// 只会触发一次重排!
|
|||
|
|
});
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### GPU 加速动画
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
const element = document.querySelector('.animated-element');
|
|||
|
|
|
|||
|
|
// 动画开始前启用 GPU 加速
|
|||
|
|
optimizer.enableGPU(element);
|
|||
|
|
|
|||
|
|
// 执行动画
|
|||
|
|
element.style.transform = 'translateX(100px)';
|
|||
|
|
|
|||
|
|
// 动画结束后禁用 GPU 加速(释放资源)
|
|||
|
|
element.addEventListener('transitionend', () => {
|
|||
|
|
optimizer.disableGPU(element);
|
|||
|
|
}, { once: true });
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 复杂场景示例
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
// 瀑布流布局重新计算
|
|||
|
|
function recalculateWaterfallLayout() {
|
|||
|
|
const items = document.querySelectorAll('.waterfall-item');
|
|||
|
|
const itemHeights = [];
|
|||
|
|
const columnHeights = [0, 0, 0]; // 3 列
|
|||
|
|
|
|||
|
|
// 第一步:批量读取所有元素高度
|
|||
|
|
optimizer.read(() => {
|
|||
|
|
items.forEach(item => {
|
|||
|
|
itemHeights.push(item.offsetHeight);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 第二步:批量写入所有元素位置
|
|||
|
|
optimizer.write(() => {
|
|||
|
|
items.forEach((item, index) => {
|
|||
|
|
// 找到最短的列
|
|||
|
|
const minColumn = columnHeights.indexOf(Math.min(...columnHeights));
|
|||
|
|
|
|||
|
|
// 设置元素位置
|
|||
|
|
item.style.position = 'absolute';
|
|||
|
|
item.style.left = (minColumn * 33.33) + '%';
|
|||
|
|
item.style.top = columnHeights[minColumn] + 'px';
|
|||
|
|
|
|||
|
|
// 更新列高度
|
|||
|
|
columnHeights[minColumn] += itemHeights[index];
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## API 参考
|
|||
|
|
|
|||
|
|
### `read(readFn)`
|
|||
|
|
|
|||
|
|
将 DOM 读取操作加入队列。
|
|||
|
|
|
|||
|
|
**参数:**
|
|||
|
|
- `readFn` (Function): 读取函数,应该只包含 DOM 读取操作(如 `offsetHeight`、`scrollTop` 等)
|
|||
|
|
|
|||
|
|
**返回值:** `void`
|
|||
|
|
|
|||
|
|
### `write(writeFn)`
|
|||
|
|
|
|||
|
|
将 DOM 写入操作加入队列。
|
|||
|
|
|
|||
|
|
**参数:**
|
|||
|
|
- `writeFn` (Function): 写入函数,应该只包含 DOM 写入操作(如修改 `style`、`className` 等)
|
|||
|
|
|
|||
|
|
**返回值:** `void`
|
|||
|
|
|
|||
|
|
### `enableGPU(element)`
|
|||
|
|
|
|||
|
|
为元素启用 GPU 加速提示。
|
|||
|
|
|
|||
|
|
**参数:**
|
|||
|
|
- `element` (Element): 需要加速的 DOM 元素
|
|||
|
|
|
|||
|
|
**返回值:** `void`
|
|||
|
|
|
|||
|
|
**注意:** 不要滥用此功能,过多的合成层会占用大量内存。
|
|||
|
|
|
|||
|
|
### `disableGPU(element)`
|
|||
|
|
|
|||
|
|
移除元素的 GPU 加速提示。
|
|||
|
|
|
|||
|
|
**参数:**
|
|||
|
|
- `element` (Element): DOM 元素
|
|||
|
|
|
|||
|
|
**返回值:** `void`
|
|||
|
|
|
|||
|
|
## 性能优势
|
|||
|
|
|
|||
|
|
### 布局抖动消除
|
|||
|
|
|
|||
|
|
**优化前:**
|
|||
|
|
```javascript
|
|||
|
|
// 交替读写,触发 4 次重排
|
|||
|
|
const h1 = elem1.offsetHeight; // 读 → 重排
|
|||
|
|
elem1.style.height = h1 + 10 + 'px'; // 写
|
|||
|
|
|
|||
|
|
const h2 = elem2.offsetHeight; // 读 → 重排
|
|||
|
|
elem2.style.height = h2 + 10 + 'px'; // 写
|
|||
|
|
|
|||
|
|
const h3 = elem3.offsetHeight; // 读 → 重排
|
|||
|
|
elem3.style.height = h3 + 10 + 'px'; // 写
|
|||
|
|
|
|||
|
|
const h4 = elem4.offsetHeight; // 读 → 重排
|
|||
|
|
elem4.style.height = h4 + 10 + 'px'; // 写
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**优化后:**
|
|||
|
|
```javascript
|
|||
|
|
// 批量读写,只触发 1 次重排
|
|||
|
|
let h1, h2, h3, h4;
|
|||
|
|
|
|||
|
|
optimizer.read(() => {
|
|||
|
|
h1 = elem1.offsetHeight;
|
|||
|
|
h2 = elem2.offsetHeight;
|
|||
|
|
h3 = elem3.offsetHeight;
|
|||
|
|
h4 = elem4.offsetHeight;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
optimizer.write(() => {
|
|||
|
|
elem1.style.height = h1 + 10 + 'px';
|
|||
|
|
elem2.style.height = h2 + 10 + 'px';
|
|||
|
|
elem3.style.height = h3 + 10 + 'px';
|
|||
|
|
elem4.style.height = h4 + 10 + 'px';
|
|||
|
|
});
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**性能提升:** 减少 75% 的重排次数!
|
|||
|
|
|
|||
|
|
## 最佳实践
|
|||
|
|
|
|||
|
|
1. **分离读写操作**:始终将读取和写入操作分开
|
|||
|
|
2. **避免在循环中使用**:不要在循环内部调用 `read()` 或 `write()`
|
|||
|
|
3. **合理使用 GPU 加速**:只在动画元素上使用,动画结束后及时移除
|
|||
|
|
4. **结合事件节流**:在高频事件(如 scroll、resize)中配合使用节流机制
|
|||
|
|
|
|||
|
|
## 测试
|
|||
|
|
|
|||
|
|
运行测试文件验证功能:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# 在浏览器中打开
|
|||
|
|
argon-performance.test.html
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
测试包括:
|
|||
|
|
- ✓ 读写队列正常工作
|
|||
|
|
- ✓ 批量操作顺序正确(先读后写)
|
|||
|
|
- ✓ GPU 加速属性设置和移除
|
|||
|
|
- ✓ 错误处理机制
|
|||
|
|
|
|||
|
|
## 相关需求
|
|||
|
|
|
|||
|
|
- **需求 2.3**: 批量读取 DOM 属性,避免布局抖动
|
|||
|
|
- **需求 2.4**: 批量写入 DOM,在读取完成后统一执行
|
|||
|
|
- **需求 17.1**: 批量读取所有需要的属性
|
|||
|
|
- **需求 17.2**: 在读取完成后批量写入
|
|||
|
|
|
|||
|
|
## 技术原理
|
|||
|
|
|
|||
|
|
### 布局抖动(Layout Thrashing)
|
|||
|
|
|
|||
|
|
当 JavaScript 交替读写 DOM 时,浏览器会被迫多次计算布局:
|
|||
|
|
|
|||
|
|
1. 读取属性(如 `offsetHeight`)→ 浏览器计算布局
|
|||
|
|
2. 修改样式 → 标记布局为脏
|
|||
|
|
3. 再次读取属性 → 浏览器重新计算布局
|
|||
|
|
4. 再次修改样式 → 再次标记为脏
|
|||
|
|
5. ...循环往复
|
|||
|
|
|
|||
|
|
### 批量处理原理
|
|||
|
|
|
|||
|
|
`ArgonRenderOptimizer` 使用 `requestAnimationFrame` 将所有操作延迟到下一帧:
|
|||
|
|
|
|||
|
|
1. 收集所有读取操作到 `readQueue`
|
|||
|
|
2. 收集所有写入操作到 `writeQueue`
|
|||
|
|
3. 在下一帧统一执行:
|
|||
|
|
- 先执行所有读取操作
|
|||
|
|
- 再执行所有写入操作
|
|||
|
|
4. 浏览器只需计算一次布局
|
|||
|
|
|
|||
|
|
### GPU 加速原理
|
|||
|
|
|
|||
|
|
`will-change` 属性提示浏览器:
|
|||
|
|
|
|||
|
|
1. 为元素创建独立的合成层(Composite Layer)
|
|||
|
|
2. 将该层的渲染交给 GPU 处理
|
|||
|
|
3. 动画时只需更新该层,不影响其他元素
|
|||
|
|
4. 大幅提升动画性能(60fps)
|
|||
|
|
|
|||
|
|
## 注意事项
|
|||
|
|
|
|||
|
|
⚠️ **不要滥用 GPU 加速**
|
|||
|
|
|
|||
|
|
每个合成层都会占用内存,过多的层会导致:
|
|||
|
|
- 内存占用过高
|
|||
|
|
- 反而降低性能
|
|||
|
|
- 移动设备可能崩溃
|
|||
|
|
|
|||
|
|
建议:
|
|||
|
|
- 只在动画元素上使用
|
|||
|
|
- 动画结束后立即移除
|
|||
|
|
- 同时运行的动画不超过 3 个
|
|||
|
|
|
|||
|
|
⚠️ **错误处理**
|
|||
|
|
|
|||
|
|
`_flush()` 方法会捕获并记录错误,但不会中断其他操作的执行。
|
|||
|
|
|
|||
|
|
## 更新日志
|
|||
|
|
|
|||
|
|
### 2026-01-16
|
|||
|
|
- ✅ 实现 ArgonRenderOptimizer 类
|
|||
|
|
- ✅ 实现读写队列和批量处理
|
|||
|
|
- ✅ 实现 GPU 加速管理
|
|||
|
|
- ✅ 添加完整的测试用例
|
|||
|
|
- ✅ 添加错误处理机制
|