You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何避免长DOM更新时HTML <progress>进度条冻结?

解决大数组处理导致UI冻结的问题

核心原因

浏览器主线程是UI渲染与JS执行共用的,你当前的Brotli解码、大数组遍历+DOM更新都是同步占满主线程的操作,导致进度条这类UI元素完全没有机会获得渲染时间,直接陷入冻结状态。

解决方案

1. 定时器分批处理(经典可靠方案)

不要一次性遍历完整数组,将数据拆分为小批次,每处理完一批就通过setTimeout或requestIdleCallback释放主线程,让UI有时间更新。

示例代码:

document.addEventListener("DOMContentLoaded", async () => {
  const progress = document.querySelector('progress');
  // 加载并解码文件
  const response = await fetch("foo.br");
  const arrayBuffer = await response.arrayBuffer();
  const decodedText = new TextDecoder().decode(new BrotliDecompress(arrayBuffer));
  const lines = decodedText.split('\n');
  
  let index = 0;
  const batchSize = 100; // 每批处理行数,可根据性能调整

  function processBatch() {
    const end = Math.min(index + batchSize, lines.length);
    // 处理当前批次
    for (; index < end; index++) {
      const div = document.createElement('div');
      div.textContent = lines[index];
      document.body.appendChild(div);
    }
    // 更新进度(可改为确定进度)
    progress.value = (index / lines.length) * 100;
    if (index < lines.length) {
      // 释放主线程,让UI渲染
      setTimeout(processBatch, 0);
      // 或用requestIdleCallback,在浏览器空闲时处理更高效
      // requestIdleCallback(processBatch);
    } else {
      progress.hidden = true;
    }
  }

  processBatch();
});

2. await+Promise拆分同步任务

单纯改用await无法直接解决问题,但如果把同步耗时操作拆分为多个Promise并配合await,就能主动释放主线程。本质和定时器方案逻辑一致,只是用Promise包装了异步等待:

async function processLines(lines) {
  let index = 0;
  const batchSize = 100;
  while (index < lines.length) {
    const end = Math.min(index + batchSize, lines.length);
    // 处理当前批次DOM更新
    for (; index < end; index++) {
      const div = document.createElement('div');
      div.textContent = lines[index];
      document.body.appendChild(div);
    }
    // 等待宏任务完成,确保UI有时间渲染
    await new Promise(resolve => setTimeout(resolve, 0));
  }
}

document.addEventListener("DOMContentLoaded", async () => {
  const progress = document.querySelector('progress');
  const response = await fetch("foo.br");
  const arrayBuffer = await response.arrayBuffer();
  const decodedText = new TextDecoder().decode(new BrotliDecompress(arrayBuffer));
  const lines = decodedText.split('\n');
  
  await processLines(lines);
  progress.hidden = true;
});

3. Web Worker彻底解放主线程

如果Brotli解码本身就耗时极高,建议把解码、数组拆分这类计算密集型操作放到Web Worker中执行,主线程只负责接收小批次数据并更新DOM。这种方式能完全避免主线程阻塞,进度条会保持流畅。

主线程代码:

document.addEventListener("DOMContentLoaded", () => {
  const progress = document.querySelector('progress');
  const worker = new Worker('decoder-worker.js');
  
  worker.postMessage('foo.br');
  
  worker.onmessage = (e) => {
    if (e.data.type === 'batch') {
      // 处理当前批次DOM更新
      e.data.content.forEach(line => {
        const div = document.createElement('div');
        div.textContent = line;
        document.body.appendChild(div);
      });
      progress.value = e.data.progress;
    } else if (e.data.type === 'done') {
      progress.hidden = true;
      worker.terminate();
    }
  };
});

Worker文件(decoder-worker.js):

self.onmessage = async (e) => {
  const response = await fetch(e.data);
  const arrayBuffer = await response.arrayBuffer();
  const decodedText = new TextDecoder().decode(new BrotliDecompress(arrayBuffer));
  const lines = decodedText.split('\n');
  
  const batchSize = 100;
  let index = 0;
  while (index < lines.length) {
    const end = Math.min(index + batchSize, lines.length);
    self.postMessage({
      type: 'batch',
      content: lines.slice(index, end),
      progress: (index / lines.length) * 100
    });
    index = end;
    // 给主线程留处理时间
    await new Promise(resolve => setTimeout(resolve, 0));
  }
  self.postMessage({type: 'done'});
};

针对你的疑问解答

  • 是否必须用定时器分批? 不是必须,但这是兼容性最好、实现最简单的方案。Web Worker是更彻底的优化方式,但需要额外的文件拆分逻辑。
  • 改用await模式是否有效? 单纯将.then()改为await没用——await只是让异步流程写法更像同步,但如果解码、遍历还是同步占满主线程,UI依然会冻结。只有将同步任务拆分为多个带await的异步块(配合setTimeout或Promise),才能主动释放主线程,让UI更新。

内容的提问来源于stack exchange,提问作者Garret Wilson

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.15 06:05:28