LOGO 首页 OA教程 ERP教程 模切知识交流 PMS教程 CRM教程 技术文档 其他文档  
 
网站管理员

从卡顿到顺滑,只差这几个优化

zhenglin
2026年6月4日 10:8 本文热度 83

同样的功能,为什么别人的网页纵享丝滑,你的却像在嚼炫迈?

今天聊聊,怎么让网页从"卡"变成"顺"。

网页为什么会卡?

一条工厂流水线

浏览器渲染网页,就像一条工厂流水线:

原材料(HTML/CSS)

    ↓

加工(DOM + CSSOM = 渲染树)

    ↓

组装(布局/排版)

    ↓

上色(绘制)

    ↓

打包出厂(合成)

任何一个环节变慢,最终产品出来就慢。

四个拖慢流水线效率的环节

问题原因结果
重排(Reflow)改了个宽高整个流水线重新跑
重绘(Repaint)改了个颜色只需要重新上色
长任务(Long Task)JS 执行太久主线程被卡住
内存泄漏废料没清理车间越来越挤

主线程是什么?

流水线再快,也要工人来操作。主线程就是这个工人。

JS 代码、DOM 操作、样式计算、布局绘制——都要这个工人来干。

如果工人在干一件大事(长 JS 任务),其他人就只能排队等着。




重排重绘:流水线上的返工

什么是重排和重绘?

流水线上的产品,刚上完色又要改尺寸,刚改完尺寸又要换颜色——这就是返工。

重排(Reflow) = 刚组装完又要拆开重装,整个车间重新排

重绘(Repaint) = 只换了个颜色喷漆,不用动组装线

# 重排:刚组装完,发现尺寸错了,整个车间要重来

div.style.width = '200px'


# 重绘:只是换个颜色,喷漆工换个颜料就行

div.style.color = 'red'

哪些操作会触发重排?

不是所有操作都会触发重排。下面这些会:

# 元素尺寸相关

offsetWidth、offsetHeight、offsetTop、getComputedStyle()

→ 读取布局信息,浏览器需要返回准确值,强制触发重排


# 增删改元素

appendChild、removeChild、display: none

→ 改变 DOM 结构


# 改尺寸位置

width、height、margin、padding、left、top

→ 直接影响布局


# 浏览器内部优化:批量处理

div.style.width = '100px'

div.style.height = '200px'

→ 连续改两次尺寸,浏览器会合并成一次重排

强制同步布局:流水线的"加急单"

正常情况下,工人按顺序干活。但如果你突然插队问"现在产品到哪了",工人就得停下手里的活,去查状态。

这就是强制同步布局(Layout Thrashing)

// ❌ 先写后读,触发强制同步布局

div.style.width = '100px'  // 工人干活中

console.log(div.offsetHeight) // 突然问尺寸,工人停下手去量

div.style.height = '200px'  // 又写,工人又要重排

读 → 写 → 读 = 两次重排,工人都没法好好干活。

// ✅ 先读后写,读完再批量写

const height = div.offsetHeight  // 先一口气读完所有尺寸

div.style.width = '100px'       // 再一口气写完

div.style.height = height + 'px'


怎么减少返工?

用 transform 代替位置变化

# ❌ 改 left/top = 刚摆好位置又搬走,整个车间重排

element.style.left = '100px'

element.style.top = '100px'


# ✅ 改 transform = 用传送带调位置,GPU直接处理,不触发重排

element.style.transform = 'translate(100px, 100px)'


transform 和 opacity 为什么不触发重排?因为它们只需要 GPU 修改,不需要 CPU 重新算布局。

批量操作 DOM

# ❌ 每次 append = 传送带停一次,车间重新算一次

div.appendChild(p1)

div.appendChild(p2)

div.appendChild(p3)


# ✅ 一次操作完再显示 = 先停传送带,统一放上去,再开

container.style.display = 'none'

container.appendChild(p1)

container.appendChild(p2)

container.appendChild(p3)

container.style.display = 'block'


用 DocumentFragment

// 虚拟操作,最后一次性插入,触发一次重排

const fragment = document.createDocumentFragment()

fragment.appendChild(p1)

fragment.appendChild(p2)

fragment.appendChild(p3)

div.appendChild(fragment)


合成层:重要产品的专用通道

什么是合成层?

有些重要产品,整条流水线走下来太慢了。工厂会开辟一条专用通道,跳过前面几步,直接到最后的打包工序。

浏览器也是这样。合成层就是某些元素的专用通道,跳过布局计算,直接合成到屏幕。

普通元素:DOM 修改 → 样式计算 → 布局 → 绘制 → 合成(走完整条流水线)


合成层元素:样式计算 → 绘制 → 合成(跳过布局,GPU 直接处理)


合成层为什么快?

  1. 不占用主线程:Layout 和 Paint 在主线程,Composite 在 GPU


  2. 不影响其他元素:普通元素重排可能影响整个车间,合成层互相独立


  3. GPU 加速:transform 和 opacity 直接由 GPU 处理,CPU 解放出来


哪些情况会创建合成层?

1. transform: translate/rotate/scale(移动、旋转、缩放)

2. opacity 有变化(透明度调整)

3. will-change 提前声明了

4. position: fixed(视口固定元素)

5. video、canvas、iframe 等元素

用 will-change 申请专用通道

/* 提前跟工厂说:这个产品要走专用通道,提前准备 */

.box {

  will-change: transform;

}

合成层的副作用

专用通道虽好,但也有代价:

内存占用:每个合成层都占用显存,100 个动画元素 = 100 个合成层 = 显存爆炸

层数过多:Chrome 需要管理所有图层,过多会影响性能

所以:

  • 不要给所有元素都加 will-change

  • 动画结束了要把 will-change 移除

  • 尽量让元素"在一起"的动画共享一个合成层

/* ✅ 共享合成层:子元素和父元素一起动 */

.parent {

  will-change: transform;

}

.parent:hover .child {

  transform: scale(1.1);

}

长任务:别让工人一直干重活

什么是长任务?

流水线上的某个工人,一直干重活不休息,其他工人都得等他手里的活干完才能继续。

JS 执行一次超过 50ms,主线程就被占着,其他任务都要排队等着。

50ms 怎么来的?Google 认为 100ms 内响应用户操作,人感觉是"即时"的。所以任务如果超过 100ms 的一半(50ms),就会被人感知到卡顿。

长任务从哪来?

// 1. 大量数据计算

const result = heavyCalculate(data) // 数据处理


// 2. 复杂 DOM 操作

document.body.innerHTML = ''        // 清空页面

for (let i = 0; i < 10000; i++) {

  document.body.appendChild(createDiv())

}


// 3. 递归调用过深

function deepClone(obj) {

  return JSON.parse(JSON.stringify(obj)) // 深拷贝大对象

}

怎么拆?

1. requestIdleCallback:让工人轮着休息

// 把大任务拆成小块,工人有空就干一点

function processItems(items, callback) {

  let index = 0 // 当前处理到第几个


  function work(deadline) {

    // deadline.timeRemaining() 表示工人还有多少空闲时间

    // 循环处理,每次只干一件,直到空闲时间用完或全部处理完

    while (index < items.length && deadline.timeRemaining() > 0) {

      process(items[index])

      index++

    }


    // 还有剩下的活没干完?排到下次空闲再继续

    if (index < items.length) {

      requestIdleCallback(work) // 还有活,队列里排着

    } else {

      callback() // 全部干完了,通知调用方

    }

  }


  // 第一次执行,排队等待工人空闲

  requestIdleCallback(work)

}


2. Web Worker:另开一条生产线

主线程生产线:只管接待客人(UI 交互)

    ↓

Worker 生产线:干重活(计算、数据处理)

    ↓

主线程:接收结果,继续组装

// main.js

const worker = new Worker('processor.js')

worker.postMessage({ type: 'process', data: bigData })


worker.onmessage = (e) => {

  const result = e.data

  updateUI(result)

}


// processor.js

self.onmessage = (e) => {

  const result = heavyCalculation(e.data.data)

  self.postMessage(result)

}

3. Scheduler API:更精细的任务调度

// 把任务分解成更小的 chunk,yield 让出主线程

async function processData() {

  for (const chunk of chunks) {

    process(chunk)

    await scheduler.yield() // 让出主线程,下一帧继续

  }

}

4. 配合动画帧:requestAnimationFrame

// 配合流水线节奏,每帧干一点,不要一直占着

function animate() {

  updateSomeUI()

  requestAnimationFrame(animate)

}

内存泄漏:废料没及时清理

什么是内存泄漏?

工厂每天产生废料。该清理的没清理,越积越多,最后车间都堆满了。

网页也是这样。该回收的变量没回收,越积越多,网页越来越慢。

内存泄漏的原理

JavaScript 使用**垃圾回收器(GC)**自动清理内存。GC 通过判断对象"是否还被引用"来决定要不要回收

可达对象:还有变量引用着 → 不回收

不可达对象:没有任何引用 → 下次 GC 回收

常见的废料堆

1. 闭包引用

function createClosure() {

  let bigData = new Array(1000000) // 大数组


  return function() {

    // bigData 被闭包引用,废料清理不掉

    console.log(bigData.length)

  }

}


const fn = createClosure()

fn() // bigData 永远不会被回收

2. 全局变量

// 不用 let/const,变量会挂在 window 上,废料堆在工厂门口

function() {

  cache = new Array(1000000)  // 挂在 window 上

  window.bigData = new Array(1000000) // 同上

}

3. 事件监听没移除

window.addEventListener('resize', handler)

// 组件销毁时没 removeEventListener → 废料一直堆着

4. 定时器没清除

setInterval(() => {

  // 定时器里的变量,永远不会被回收

  process(data)

}, 1000)


// 不用了要 clearInterval

5. 闭包里的定时器

function init() {

  const largeData = new Array(1000000)


  setTimeout(() => {

    // 这个回调引用了 largeData,即使 init 执行完了

    // setTimeout 没清除,largeData 就不会被回收

    console.log(largeData.length)

  }, 10000)

}

6. console.log 的"陷阱"

// 生产环境中,console.log 可能导致内存泄漏

// 浏览器需要保留 console 引用的对象,直到控制台被清空

const bigData = new Array(1000000)

console.log(bigData) // 如果控制台一直开着,bigData 不会被回收

怎么找废料在哪?

Chrome DevTools → Memory 面板:

堆快照:拍一张某个时刻的内存照片,对比两个时刻的差异

记录时间线:看内存是怎么涨的

分配堆栈:看是谁在分配内存没回收

查找内存泄漏的步骤:

1. 打开 Memory 面板

2. 选"堆快照"

3. 操作页面(比如打开关闭弹窗)

4. 再拍一张快照

5. 对比两次快照,找内存增长点


Performance API:给车间装计数器

是什么?

每个车间门口都可以装个计数器,统计每个环节花了多少时间。

浏览器有内置的性能监控工具:

performance.now()    // 精确到微秒,比 Date.now() 更准

performance.mark()  // 记录某个时间点

performance.measure() // 测量两个时间点之间花了多久

怎么用?

// 标记开始时间

performance.mark('start')


// 你的代码

doSomething()


// 标记结束时间

performance.mark('end')


// 测量耗时

performance.measure('doSomething耗时', 'start', 'end')


// 获取测量结果

const measures = performance.getEntriesByType('measure')

console.log(measures[0].duration)

PerformanceObserver:自动观察性能

// 自动观察长任务

const observer = new PerformanceObserver((list) => {

  for (const entry of list.getEntries()) {

    if (entry.duration > 50) {

      console.log('发现长任务:', entry.name, entry.duration, 'ms')

    }

  }

})


observer.observe({ entryTypes: ['longtask'] })

火焰图:一眼看出哪慢

Chrome DevTools → Performance 面板 → 录制操作 → 生成火焰图:

火焰图怎么看:

- 横轴是时间,越宽说明越慢

- 纵轴是调用栈,越高说明调用层次越深

- 顶部是耗时最多的函数,要重点优化

Core Web Vitals

客户验收产品要看三个指标:

指标含义及格线优化方式
LCP最大那块料多久出来< 2.5s优化服务器、CDN、缓存
FID第一次有人来响应< 100ms减少长任务、分解大 JS
CLS产品摆放位置晃不晃< 0.1预留图片尺寸、固定动态元素

实战:列表页滚动优化

场景

列表页加载 1000 条数据,滚动特别卡。

分析

问题:1000 个产品各装了一个报警器,响了没人来处理

诊断:Performance 面板看到工人一直在响应报警,没空干活

第一步:事件委托

不用每个产品都装报警器,在车间门口装一个就够了。

// ❌ 1000 个产品各装一个监听

items.forEach(item => {

  item.addEventListener('click', handleClick)

})


// ✅ 事件委托,只装一个

list.addEventListener('click', (e) => {

  const item = e.target.closest('.item')

  if (item) {

    handleClick(item)

  }

})


第二步:虚拟滚动

1000 个产品其实只有 20 个在展示台上。滚动时动态换展示台上的产品。

class VirtualList {

  constructor(container, items) {

    this.container = container

    this.items = items

    this.itemHeight = 50

    this.visibleCount = Math.ceil(container.clientHeight / this.itemHeight)


    container.addEventListener('scroll', () => this.onScroll())

    this.render()

  }


  onScroll() {

    const scrollTop = this.container.scrollTop

    const startIndex = Math.floor(scrollTop / this.itemHeight)

    this.render(startIndex)

  }


  render(startIndex = 0) {

    const visibleItems = this.items.slice(

      startIndex,

      startIndex + this.visibleCount

    )


    // 只渲染可见的 20 个,而不是 1000 个

    this.container.innerHTML = visibleItems.map(item => `

      <div class="item" style="height: ${this.itemHeight}px">

        ${item.name}

      </div>

    `).join('')


    this.container.scrollTop = this.container.scrollTop

  }

}

第三步:懒加载图片

用 IntersectionObserver 检测,快到展示台了再搬过来。

const observer = new IntersectionObserver((entries) => {

  entries.forEach(entry => {

    if (entry.isIntersecting) {

      const img = entry.target

      img.src = img.dataset.src // 开始加载真实图片

      observer.unobserve(img)   // 加载完就取消观察

    }

  })

}, {

  rootMargin: '100px' // 提前 100px 开始加载

})


document.querySelectorAll('img[data-src]').forEach(img => {

  observer.observe(img)

})

效果

滚动帧率从 15fps 提到 60fps。


写在最后

网页卡顿不是玄学,是有具体原因的。

记住这几点:

少返工 → 用 transform 代替位置,读写分离

走专用通道 → will-change 声明,及时移除

别让工人一直干重活 → 分解长任务,Worker 离线计算

废料及时清理 → 移除无用监听,定时器,闭包

用计数器找问题 → Performance API,火焰图

下次网页卡了,别急着怪电脑配置,先看看代码。


阅读原文


该文章在 2026/6/4 10:08:21 编辑过
关键字查询
相关文章
正在查询...
点晴ERP是一款针对中小制造业的专业生产管理软件系统,系统成熟度和易用性得到了国内大量中小企业的青睐。
点晴PMS码头管理系统主要针对港口码头集装箱与散货日常运作、调度、堆场、车队、财务费用、相关报表等业务管理,结合码头的业务特点,围绕调度、堆场作业而开发的。集技术的先进性、管理的有效性于一体,是物流码头及其他港口类企业的高效ERP管理信息系统。
点晴WMS仓储管理系统提供了货物产品管理,销售管理,采购管理,仓储管理,仓库管理,保质期管理,货位管理,库位管理,生产管理,WMS管理系统,标签打印,条形码,二维码管理,批号管理软件。
点晴免费OA是一款软件和通用服务都免费,不限功能、不限时间、不限用户的免费OA协同办公管理系统。
Copyright 2010-2026 ClickSun All Rights Reserved  粤ICP备13012886号-2  粤公网安备44030602007207号