108 lines
3.2 KiB
Markdown
108 lines
3.2 KiB
Markdown
---
|
|
title: Avoid Layout Thrashing
|
|
impact: MEDIUM
|
|
impactDescription: 防止强制同步布局,减少性能瓶颈
|
|
tags: javascript, dom, css, performance, reflow, layout-thrashing
|
|
---
|
|
|
|
## 避免布局抖动
|
|
|
|
避免将样式写入与布局读取交错进行。当你在样式更改之间读取布局属性(如 `offsetWidth`、`getBoundingClientRect()` 或 `getComputedStyle()`)时,浏览器会被迫触发同步重排。
|
|
|
|
**这样是可以的(浏览器会批量处理样式更改):**
|
|
```typescript
|
|
function updateElementStyles(element: HTMLElement) {
|
|
// 每一行都会使样式失效,但浏览器会批量重新计算
|
|
element.style.width = '100px'
|
|
element.style.height = '200px'
|
|
element.style.backgroundColor = 'blue'
|
|
element.style.border = '1px solid black'
|
|
}
|
|
```
|
|
|
|
**错误做法(读写交错会强制重排):**
|
|
```typescript
|
|
function layoutThrashing(element: HTMLElement) {
|
|
element.style.width = '100px'
|
|
const width = element.offsetWidth // 强制重排
|
|
element.style.height = '200px'
|
|
const height = element.offsetHeight // 再次强制重排
|
|
}
|
|
```
|
|
|
|
**正确做法(批量写入,然后一次性读取):**
|
|
```typescript
|
|
function updateElementStyles(element: HTMLElement) {
|
|
// 将所有写入操作批量处理
|
|
element.style.width = '100px'
|
|
element.style.height = '200px'
|
|
element.style.backgroundColor = 'blue'
|
|
element.style.border = '1px solid black'
|
|
|
|
// 所有写入完成后读取(仅一次重排)
|
|
const { width, height } = element.getBoundingClientRect()
|
|
}
|
|
```
|
|
|
|
**正确做法(批量读取,然后再写入):**
|
|
```typescript
|
|
function avoidThrashing(element: HTMLElement) {
|
|
// 读取阶段——先执行所有布局查询
|
|
const rect1 = element.getBoundingClientRect()
|
|
const offsetWidth = element.offsetWidth
|
|
const offsetHeight = element.offsetHeight
|
|
|
|
// 写入阶段——之后再执行所有样式更改
|
|
element.style.width = '100px'
|
|
element.style.height = '200px'
|
|
}
|
|
```
|
|
|
|
**更好的做法:使用 CSS 类**
|
|
```css
|
|
.highlighted-box {
|
|
width: 100px;
|
|
height: 200px;
|
|
background-color: blue;
|
|
border: 1px solid black;
|
|
}
|
|
```
|
|
```typescript
|
|
function updateElementStyles(element: HTMLElement) {
|
|
element.classList.add('highlighted-box')
|
|
|
|
const { width, height } = element.getBoundingClientRect()
|
|
}
|
|
```
|
|
|
|
**React 示例:**
|
|
```tsx
|
|
// 错误做法:将样式更改与布局查询交错进行
|
|
function Box({ isHighlighted }: { isHighlighted: boolean }) {
|
|
const ref = useRef<HTMLDivElement>(null)
|
|
|
|
useEffect(() => {
|
|
if (ref.current && isHighlighted) {
|
|
ref.current.style.width = '100px'
|
|
const width = ref.current.offsetWidth // 强制布局
|
|
ref.current.style.height = '200px'
|
|
}
|
|
}, [isHighlighted])
|
|
|
|
return <div ref={ref}>Content</div>
|
|
}
|
|
|
|
// 正确做法:切换类名
|
|
function Box({ isHighlighted }: { isHighlighted: boolean }) {
|
|
return (
|
|
<div className={isHighlighted ? 'highlighted-box' : ''}>
|
|
Content
|
|
</div>
|
|
)
|
|
}
|
|
```
|
|
|
|
尽可能优先使用 CSS 类而非内联样式。CSS 文件会被浏览器缓存,类能提供更好的关注点分离,且更易于维护。
|
|
|
|
更多关于强制布局操作的信息,请参见[此 Gist](https://gist.github.com/paulirish/5d52fb081b3570c81e3a) 和 [CSS Triggers](https://csstriggers.com/)。
|