Web Worker 的工作原理(实际示例)

2025-06-11

Web Worker 的工作原理(实际示例)

您是否曾注意到网页在执行繁重任务时卡顿?这是因为 JavaScript 默认在单线程上运行,导致用户体验不佳。用户无法进行交互,只能等待任务完成。这个问题可以通过使用 Web Worker 来解决。在本文中,我们将通过一个实际示例(构建一个图像压缩应用程序)来探讨 Web Worker 是什么、它们为何如此有用以及如何使用它们。是不是感觉很刺激?那就开始吧。

什么是 Web Worker?

Web Workers 允许 JavaScript 在后台运行任务,而不会阻塞主线程,从而保持 UI 的流畅性和响应速度。您可以使用 Web Workers API 创建它们,它接受两个参数:urloptions。以下是创建 Web Workers 的一个简单示例。

const worker = new Worker('./worker.js', { type: 'module' });
Enter fullscreen mode Exit fullscreen mode

为什么要使用 Web Worker?

如前所述,Web Worker 在后台运行任务。以下是使用它们的几个理由

  • 防止在繁重计算期间页面滞后

  • 高效处理大数据

  • 提高复杂 Web 应用程序的性能

它们是如何工作的?

  1. 主线程创建一个worker并赋予它一个任务

  2. 工作人员在后台处理任务

  3. 完成后,将结果发送回主线程

Web Worker 的工作原理

好了,现在我们知道了什么是 Web Worker,为什么要用它,以及它是如何工作的。但这还不够,对吧?接下来,让我们构建一个图像压缩应用程序,看看如何在实践中运用 Web Worker。

项目设置

使用 TypeScript 和 Tailwind CSS 创建 Next.js 项目

npx create-next-app@latest --typescript web-worker-with-example

cd web-worker-with-example
Enter fullscreen mode Exit fullscreen mode

创建项目

为了在浏览器中压缩图像,我们将使用@jsquash/webnpm 库来编码和解码 WebP 图像。该库由 WebAssembly 提供支持,因此我们先安装它。

npm install @jsquash/webp
Enter fullscreen mode Exit fullscreen mode

太好了,我们的项目设置完成了。在下一节中,我们将创建一个工作脚本来管理图像压缩。

创建工作者脚本

工作者脚本是一个 JavaScript 或 TypeScript 文件,其中包含处理工作者消息事件的代码。

在文件夹内创建一个imageCompressionWorker.ts文件src/worker并添加以下代码。

/// <reference lib="webworker" />

const ctx = self as DedicatedWorkerGlobalScope;

import { decode, encode } from '@jsquash/webp';

ctx.onmessage = async (
  event: MessageEvent<{
    id: number;
    imageFile: File;
    options: { quality: number };
  }>
) => {
  // make sure the wasm is loaded
  await import('@jsquash/webp');

  const { imageFile, options, id } = event.data;
  const fileBuffer = await imageFile.arrayBuffer();
  try {
    const imageData = await decode(fileBuffer);
    const compressedBuffer = await encode(imageData, options);
    const compressedBlob = new Blob([compressedBuffer], {
      type: imageFile.type,
    });
    ctx.postMessage({ id, blob: compressedBlob });
  } catch (error: unknown) {
    const message = error instanceof Error ? error.message : 'Unknown error';
    ctx.postMessage({ id, error: message });
  }
};

export {};
Enter fullscreen mode Exit fullscreen mode

在这里,我们从库中导入encode和方法,并使用工作者全局范围来监听来自主线程的消息。decode@jsquash/webpself

当消息到达时,我们会获取图像文件和选项,然后通过先解码再使用质量选项进行编码来压缩图像。最后,我们使用postMessage将压缩后的图像 blob 发送回主线程。如果出现错误,我们会处理错误并使用 发送错误消息postMessage

工作脚本已准备就绪。在下一节中,我们将构建 Imagelist 组件、更新样式、更新页面,并使用工作脚本处理压缩。

使用 Web Worker

在开始之前,让我们global.css使用以下内容更新文件并删除默认样式。

@tailwind base;
@tailwind components;
@tailwind utilities;
Enter fullscreen mode Exit fullscreen mode

ImageList.tsx在文件夹中创建一个src/components并添加以下代码。

/* eslint-disable @next/next/no-img-element */
import React from 'react';

export type ImgData = {
  id: number;
  file: File;
  status: 'compressing' | 'done' | 'error';
  originalUrl: string;
  compressedUrl?: string;
  error?: string;
  compressedSize?: number;
};

interface ImageListProps {
  images: ImgData[];
}

const formatBytes = (bytes: number): string => {
  if (bytes === 0) return '0 Bytes';
  const k = 1024;
  const dm = 2;
  const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
  const i = Math.floor(Math.log(bytes) / Math.log(k));
  return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
};

const ImageList: React.FC<ImageListProps> = ({ images }) => {
  return (
    <div className="mt-4">
      <h2 className="text-xl font-semibold mb-2">Image List</h2>
      <div className="space-y-4">
        {images.map((img) => (
          <div
            key={img.id}
            className="flex flex-col md:flex-row items-center border p-4 rounded"
          >
            <div className="flex-1 flex flex-col items-center">
              <p className="font-bold mb-2">Original</p>
              <img
                src={img.originalUrl}
                alt="Original"
                className="w-32 h-32 object-cover rounded border mb-2"
              />
              <p className="text-sm">Size: {formatBytes(img.file.size)}</p>
            </div>
            {img.status === 'done' && img.compressedUrl ? (
              <div className="flex-1 flex flex-col items-center mt-4 md:mt-0">
                <p className="font-bold mb-2">Compressed</p>
                <img
                  src={img.compressedUrl}
                  alt="Compressed"
                  className="w-32 h-32 object-cover rounded border mb-2"
                />
                <p className="text-sm">
                  Size:{' '}
                  {img.compressedSize ? formatBytes(img.compressedSize) : 'N/A'}
                </p>
                <a
                  href={img.compressedUrl}
                  download={`${img.file.name.replace(
                    /\.[^/.]+$/,
                    ''
                  )}-compressed.webp`}
                  className="mt-2 inline-block px-3 py-1 bg-blue-500 text-white rounded"
                >
                  Download
                </a>
              </div>
            ) : img.status === 'compressing' ? (
              <div className="flex-1 flex flex-col items-center mt-4 md:mt-0">
                <p className="font-bold">Compressing...</p>
              </div>
            ) : img.status === 'error' ? (
              <div className="flex-1 flex flex-col items-center mt-4 md:mt-0">
                <p className="font-bold text-red-500">Error in compression</p>
              </div>
            ) : null}
          </div>
        ))}
      </div>
    </div>
  );
};

export default ImageList;
Enter fullscreen mode Exit fullscreen mode

ImageList 组件接受一个 prop,images即 ,它是一个 列表ImgData。然后,它显示原始图像和压缩图像,显示它们的大小并提供压缩图像的下载选项。

接下来,app/page.tsx用下面的代码更新,让我们一起来看看各个部分。

'use client';

import { useState, useRef, useEffect } from 'react';
import ImageList, { ImgData } from '../components/ImageList';

export default function Home() {
  const [images, setImages] = useState<ImgData[]>([]);
  const [text, setText] = useState('');

  const workerRef = useRef<Worker | null>(null);

  const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const files = e.target.files;
    if (!files || !workerRef.current) return;
    const filesArray = Array.from(files);
    filesArray.forEach((file, index) => {
      const id = Date.now() + index;

      const originalUrl = URL.createObjectURL(file);
      setImages((prev) => [
        ...prev,
        { id, file, status: 'compressing', originalUrl },
      ]);

      // Send the file with its id to the worker.
      workerRef.current!.postMessage({
        id,
        imageFile: file,
        options: { quality: 75 },
      });
    });
  };

  // Initialize the worker once when the component mounts.
  useEffect(() => {
    const worker = new Worker(
      new URL('../worker/imageCompressionWorker.ts', import.meta.url),
      { type: 'module' }
    );
    workerRef.current = worker;

    // Listen for messages from the worker.
    worker.onmessage = (
      event: MessageEvent<{ id: number; blob?: Blob; error?: string }>
    ) => {
      const { id, blob: compressedBlob, error } = event.data;
      setImages((prev) =>
        prev.map((img) => {
          if (img.id === id) {
            if (error) return { ...img, status: 'error', error };
            const compressedSize = compressedBlob!.size;
            const compressedUrl = URL.createObjectURL(compressedBlob!);
            return { ...img, status: 'done', compressedUrl, compressedSize };
          }
          return img;
        })
      );
    };

    return () => {
      worker.terminate();
    };
  }, []);

  return (
    <div className="min-h-screen p-8">
      <h1 className="text-2xl font-bold text-center mb-4">
        Image Compression with Web Workers
      </h1>
      <div className="rounded shadow p-4 mb-4 flex flex-col gap-2">
        <p className="text-sm">
          While images are compressing, you can interact with the textarea below
          and observe the text being typed and UI is not frozen.
        </p>
        <p className="text-sm">
          Even you can open the dev tools and then open the performance tab and
          see the INP(Interaction to Next Paint) is very low.
        </p>
        <textarea
          className="w-full h-32 border rounded p-2 text-black"
          placeholder="Type here while images are compressing..."
          value={text}
          onChange={(e) => setText(e.target.value)}
        ></textarea>
      </div>
      <div className="rounded shadow p-4">
        <input
          type="file"
          multiple
          accept="image/webp"
          onChange={handleFileChange}
        />
        <ImageList images={images} />
      </div>
    </div>
  );
} 
Enter fullscreen mode Exit fullscreen mode

首先,我们导入钩子和 ImageList 组件,以及 ImgData 类型。

import { useState, useRef, useEffect } from 'react';
import ImageList, { ImgData } from '../components/ImageList';
Enter fullscreen mode Exit fullscreen mode

然后,我们创建一个 ref 来保存 Worker 实例,因为我们不想在每次重新渲染时重复创建 Worker。我们也希望避免在 Worker 实例发生变化时重新渲染组件。

const workerRef = useRef<Worker | null>(null);
Enter fullscreen mode Exit fullscreen mode

在 useEffect 中,我们使用imageCompressionWorker.ts之前创建的 worker 脚本来初始化 worker 实例。

我们使用 URL API import.meta.url。这使得路径相对于当前脚本而不是 HTML 页面。这样,打包器就可以安全地进行优化,例如重命名,因为否则,URLworker.js可能会指向打包器未管理的文件,从而阻止其进行任何假设。点击此处了解更多信息。

一旦 Worker 初始化完毕,我们就会监听来自它的消息。收到消息后,我们会提取 id、blob 和 error,然后用新值更新图片状态。

最后,当组件卸载时,我们清理工作器。

useEffect(() => {
    const worker = new Worker(
      new URL('../worker/imageCompressionWorker.ts', import.meta.url),
      { type: 'module' }
    );
    workerRef.current = worker;

    // Listen for messages from the worker.
    worker.onmessage = (
      event: MessageEvent<{ id: number; blob?: Blob; error?: string }>
    ) => {
      const { id, blob: compressedBlob, error } = event.data;
      setImages((prev) =>
        prev.map((img) => {
          if (img.id === id) {
            if (error) return { ...img, status: 'error', error };
            const compressedSize = compressedBlob!.size;
            const compressedUrl = URL.createObjectURL(compressedBlob!);
            return { ...img, status: 'done', compressedUrl, compressedSize };
          }
          return img;
        })
      );
    };

    return () => {
      worker.terminate();
    };
  }, []);
Enter fullscreen mode Exit fullscreen mode

为了管理图片文件的上传,我们使用handleFileChange方法来处理。该方法监听onchange文件输入事件,处理文件并将其发送给 Worker 进行压缩。

const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const files = e.target.files;
    if (!files || !workerRef.current) return;
    const filesArray = Array.from(files);
    filesArray.forEach((file, index) => {
      const id = Date.now() + index;

      const originalUrl = URL.createObjectURL(file);
      setImages((prev) => [
        ...prev,
        { id, file, status: 'compressing', originalUrl },
      ]);

      // Send the file with its id to the worker.
      workerRef.current!.postMessage({
        id,
        imageFile: file,
        options: { quality: 75 },
      });
    });
  };
Enter fullscreen mode Exit fullscreen mode

最后,渲染元素文本区域、图像输入和图像列表。

流程图

  • 用户选择图像:用户使用文件输入选择图像,这使得组件创建对象 URL 并将每个图像标记为“压缩”。

  • 工作者通信:组件将每个图像文件(带有选项)发送到 Web 工作者。

  • 并行进程:

    • 文本区域交互:同时用户可以在文本区域进行输入,体现UI不被遮挡。
    • 图像压缩:工作人员在后台压缩图像。
  • 完成:压缩完成后,工作者将结果发送回组件,组件使用压缩图像更新 UI,同时文本区域保持平稳运行。

太好了,一切设置完毕。下一节,我们将运行该应用程序,并了解 Web Worker 的工作原理。

吉菲

运行示例

打开终端并运行以下命令,然后转到http://localhost:3000/

npm run dev
Enter fullscreen mode Exit fullscreen mode

工作示例的屏幕截图1

工作示例的屏幕截图2

在此处尝试现场演示:https://web-worker-with-example.vercel.app/

结论

Web Workers 是提升应用程序性能的绝佳工具。使用 Web Workers,您可以确保应用程序运行更快、更流畅、响应更灵敏。但是,不应过度使用,仅在必要时使用。此外,请检查浏览器支持情况,目前全球浏览器支持率约为 98%。您可以点击此处查看。

本期内容就到这里。感谢您的阅读!如果您觉得本文有帮助,请点赞、评论并与他人分享。

资源

鏂囩珷鏉ユ簮锛�https://dev.to/sachinchaurasiya/how-web-worker-works-with-a-practical-example-c98
PREV
使用 AWS(Amazon Web Services)实现无服务器 CI/CD 管道。
NEXT
等一下...React.useState 是如何工作的?