😝 5 亿次循环卡死页面?我用 Web Worker 把它扔进后台线程,页面丝滑如初

写在前面:今天学了一个让我"性能觉醒"的知识------useRef + Web Worker 。以前写代码遇到耗时计算(比如 5 亿次循环),页面直接卡死,点啥都没反应------就像你让厨师一边切菜一边收银,结果客人点单都顾不上。老师说:"JS 是单线程的,但浏览器可以给你开一个'独立后厨'------Web Worker 线程。" 把 5 亿次循环丢给 Worker,主线程继续"接待客人",计算完再通过消息机制把结果送回来。页面再也不会卡死了!


一、先认识 useRef:持久可变对象

1.1 useRef 是什么?

老师说:

"useRef 是 React 的一个提供持久可变对象的 Hook 函数,经常用来引用 DOM 节点对象。它有一个 current 属性,可以指向任何值或对象,不会触发渲染。"

javascript 复制代码
import { useRef } from 'react';

const numRef = useRef(0);  // 创建一个可变对象,初始 current = 0

useRef 返回一个对象:{ current: 初始值 }

  • numRef.current:可以读。
  • numRef.current += 1:可以改。
  • 改它不会触发渲染(和 useState 最大的区别)。

1.2 useState vs useRef

App.jsx(第一个版本)的对比代码:

jsx 复制代码
const App = () => {
    const numRef = useRef(0);   // 引用一个值(非响应式)
    const [, forceRender] = useState(0);  // 响应式

    return (
        <div onClick={() => {
            numRef.current += 1;  // 改 ref 不触发渲染
            forceRender(0);       // 手动触发渲染
        }}>
            {numRef.current}
        </div>
    );
};
对比 useState useRef
改变值 setCount() ref.current = xxx
触发渲染 ✅ 会 ❌ 不会
适用场景 数据状态、UI 相关 DOM 引用、可变对象

老师说:

"useState 聚焦数据状态业务;useRef DOM 对象引用等。useState 响应式的,useRef 非响应式的。"

一句话总结:想让数据驱动 UI → useState;想持有一个不会引起渲染的可变对象 → useRef。


二、useRef 的第一个场景:绑定 DOM

2.1 React 不做 DOM 编程

老师说:

"React/Vue 之前,原生 JS 做 DOM 编程,非常耗性能。React/Vue 新框架直接规避 DOM 编程,useState 数据绑定 + 响应式编程,前端开发方式直接改变。"

原生 JS 时代:

javascript 复制代码
document.getElementById('input').focus();  // 手动操作 DOM

React 时代:

jsx 复制代码
<input ref={inputRef} />
// 通过 ref 拿到 DOM 节点,交给 React 管理

2.2 useRef 绑定 DOM

jsx 复制代码
const App = () => {
    const inputRef = useRef(null);  // 初始 null,未来指向 DOM 节点

    useEffect(() => {
        inputRef.current.focus();   // 挂载后自动聚焦输入框
    }, []);

    return (
        <input
            type="text"
            placeholder="请输入用户名"
            ref={inputRef}          // ref 属性绑定
        />
    );
};

流程:

ini 复制代码
<input ref={inputRef} />
  ↓ React 渲染时
inputRef.current = 那个 input 的 DOM 节点
  ↓ useEffect(挂载后)
inputRef.current.focus()  → 输入框自动聚焦

用户体验:页面打开,光标直接落在输入框里,用户不用点一下就能打字。 老师说:"把用户当小白,前端的职责就是打造良好的用户体验。"


三、问题:JS 单线程,耗时任务卡死页面

3.1 JS 为什么是单线程?

老师说:

"JS 做一些前端交互、脚本工作、显示和操作的页面,一致性不能出问题。JS 如果是多线程,可能就会有冲突。"

如果两个线程同时改同一个 DOM,页面就乱了。 所以 JS 设计成单线程------所有任务排队执行(Event Loop)。

3.2 单线程的痛点

一个 5 亿次的循环:

javascript 复制代码
for (let i = 0; i < 5000000000; i++) {
    sum += num * i;
}

这段代码在主线程跑的时候:

复制代码
主线程:执行 5 亿次循环(卡住!)
  ↓
用户:滚动屏幕?没反应
用户:点击按钮?没反应
  ↓
页面就像死了一样

老师说:

"JS 主线程负责脚本执行、DOM 渲染、用户交互,忙得飞起。繁重的 CPU 计算会阻塞主线程,造成页面卡顿。"

这就好比收银台只有一个窗口------你让收银员去后厨炒菜,其他客人全等着。


四、解决方案:Web Worker 独立线程

4.1 什么是 Web Worker?

老师说:

"浏览器提供给 JS 可以调用的耗时性计算或 LLM、游戏等复杂任务的 Worker 线程。开启一个新的线程,和主线程隔离开,两个线程互不打扰,并行执行。"

Web Worker 是浏览器开辟的"独立后厨":

  • 主线程:负责 UI、交互(前台接待)。
  • Worker 线程:负责耗时计算(后厨炒菜)。
  • 两个线程并行执行,互不阻塞。

4.2 创建 Worker

javascript 复制代码
const worker = new Worker(
    new URL("./worker.js", import.meta.url)
);

new Worker() 就开启了一个新线程,加载 worker.js 里的代码。

4.3 Worker 里能做什么?

worker.js

javascript 复制代码
// web worker 独立子线程计算
// 不可以做 DOM api,有自己的 api
console.log('worker online');
// self 关键字:worker 线程自己的全局对象
self.onmessage = (e) => {
    const { num } = e.data;   // 接收主线程发来的任务
    console.log('Worker收到主线程任务', e.data);
    let sum = 0;
    for (let i = 0; i < 5000000000; i++) {  // 5 亿次循环!
        sum += num * i;
    }
    self.postMessage({   // 计算完成,把结果发回主线程
        result: sum
    });
};

Worker 的关键点:

概念 说明
self Worker 线程的全局对象(类似主线程的 window)
self.onmessage 接收主线程消息的监听器
self.postMessage() 向主线程发送消息
不能访问 DOM Worker 没有 document,不能操作页面

4.4 消息机制(通信)

主线程和 Worker 之间的通信靠"消息":

php 复制代码
主线程:worker.postMessage({ num: 88 })    → 发任务
  ↓
Worker:self.onmessage 收到任务,开始计算
  ↓
Worker:self.postMessage({ result: sum })  → 发结果
  ↓
主线程:worker.onmessage 收到结果

这种"消息机制"就像两个隔开房间的人用对讲机通话------不共享内存,只交换消息。


五、完整实战:useRef 管理 Worker 生命周期

5.1 完整代码

App.jsx(第二个版本):

jsx 复制代码
import { useRef, useEffect, useState } from 'react';

function App() {
    console.log('main thread');
    const workerRef = useRef(null);   // 持久保存 worker 实例
    const [result, setResult] = useState(null);
    const [loading, setLoading] = useState(false);

    useEffect(() => {
        // 组件挂载后,开启一个 worker 线程
        workerRef.current = new Worker(
            new URL("./worker.js", import.meta.url)
        );
        // 监听 worker 有没有消息到达
        workerRef.current.onmessage = (e) => {
            const { result } = e.data;
            setResult(result);
            setLoading(false);
        };
        // 组件卸载时,销毁 worker 线程
        return () => {
            workerRef.current.terminate();
            workerRef.current = null;
        };
    }, []);

    const startHeavyCalc = () => {
        setLoading(true);
        // 给 worker 发一条工作指令,带上参数
        workerRef.current.postMessage({
            num: 88
        });
    };

    return (
        <div style={{ padding: '30px' }}>
            <h2>useRef + Web Worker 耗时运算</h2>
            <button
                onClick={startHeavyCalc}
                disabled={loading}
            >
                {loading ? '正在后台计算...' : '启动繁重计算任务'}
            </button>
            {result && <h3>计算结果:{result}</h3>}
        </div>
    );
}

export default App;

5.2 为什么用 useRef 存 Worker?

老师说:

"useRef 用来持久存放 Web Worker 实例,组件每次渲染不会重置该线程对象。方便监听、发送数据,以及组件卸载时销毁线程。"

如果不用 useRef,用普通变量存 Worker:

  • 组件每次渲染,普通变量会重新创建。
  • Worker 实例可能会丢失或重复创建。

useRef 存 Worker:

  • workerRef.current 在组件整个生命周期内持久存在
  • 不会因为渲染而重置。

5.3 完整生命周期

阶段 代码 做了什么
挂载后 useEffectnew Worker() 创建 Worker 线程
计算时 workerRef.current.postMessage() 发送计算任务
完成时 workerRef.current.onmessage 接收结果,setState 更新
卸载时 workerRef.current.terminate() 销毁线程,回收资源

useRef + useEffect 的组合:useEffect 负责生命周期(创建/销毁),useRef 负责持久引用。


六、Web Worker 适合什么场景?

老师说:

"耗时性复杂专项任务:游戏引擎计算、LLM、加密等密集计算。"

场景 为什么用 Worker
游戏引擎 物理计算、寻路算法很耗 CPU
LLM 推理 端侧模型计算量巨大
加密解密 密集的数学运算
大数据处理 百万级数据过滤、排序

不适合用 Worker 的: 操作 DOM、需要频繁与主线程交互的任务(消息通信有开销)。


七、总结:useRef + Web Worker

概念 说明
useRef 持久可变对象,{ current },不触发渲染
useRef 绑 DOM ref={inputRef}inputRef.current.focus()
JS 单线程 Event Loop 排队执行,主线程负责 UI
主线程阻塞 耗时计算导致页面卡顿
Web Worker 浏览器开辟的独立后台线程
消息机制 postMessage 发送,onmessage 接收
worker 不能碰 DOM 只有自己的 API,用 self
useRef 存 Worker 持久保存实例,生命周期由 useEffect 管理

Web Worker 是浏览器给 JS 的"外挂线程"------把耗时的纯计算任务丢进去,主线程保持流畅。JS 本身仍然是单线程语言,Worker 只是浏览器提供的辅助线程。


写在最后

今天最大的收获,是亲手体验了"页面卡死 → Worker 救场"的全过程。5 亿次循环,以前在主线程跑,页面直接冻住;扔进 Web Worker 后,页面照常响应,计算完结果自己送回来。那种"后台默默干活,前台丝滑如初"的感觉,太爽了。

而且 useRef 这个 Hook 也让我很惊喜------之前只以为它能绑 DOM,现在知道它还能持久保存任何对象(包括 Worker 实例),配合 useEffect 管理生命周期,完美。

下次面试官问你:"useRef 和 Web Worker 怎么配合使用?"

你可以淡定地说:

"useRef 创建持久可变对象({ current }),改变它不会触发渲染,常用于引用 DOM 节点或保存不会因渲染而重置的对象。Web Worker 是浏览器提供的独立后台线程,用来执行耗时计算(如 5 亿次循环、LLM 推理),避免阻塞主线程导致页面卡顿。两者的配合是:在 useEffect 中创建 Worker 并保存到 workerRef.current,组件卸载时 terminate() 销毁;通过 postMessage 发送任务、onmessage 接收结果,用 useState 把结果更新到界面。Worker 线程不能访问 DOM,只能通过消息机制和主线程通信。JS 本身仍是单线程语言,Worker 只是浏览器的辅助线程。"

然后看着面试官满意的表情,心里默念:这波,又稳了。


本文所有代码示例均来自课堂学习资料,真实可运行。

相关推荐
CAD老兵1 小时前
一行代码集成 DWG/DXF 图纸查看:测量批注,数据不出站
前端·javascript·github
小林ixn1 小时前
单例模式:从弹窗管理到全局状态,一个模式搞定
前端·javascript·设计模式
AI编程实验室1 小时前
Node.js Agent Handoff 仓库扫描 MVP:忽略规则、include 通配与稳定输出实现
前端·ai编程
猫不易1 小时前
从 Virtual DOM 到 Vapor:Vue 3.6 的另一条路
前端·vue.js
光影少年1 小时前
react navite性能优化 & 常见坑
前端·react native·掘金·金石计划
八角丶1 小时前
Node.js Cluster 详解
前端·node.js
名字还没想好☜1 小时前
React 用 useEffect 做轮询实战:setInterval 拿到旧 state 的闭包陷阱与正确清理
前端·javascript·react.js·react·useeffect
hiahiahia1231 小时前
AI Web 项目的文件到底应该怎么放?
前端·人工智能
愚公搬代码1 小时前
【愚公系列】《Web应用安全》003-测试环境的搭建
前端·安全