Web Worker 的工作原理(实际示例)
您是否曾注意到网页在执行繁重任务时卡顿?这是因为 JavaScript 默认在单线程上运行,导致用户体验不佳。用户无法进行交互,只能等待任务完成。这个问题可以通过使用 Web Worker 来解决。在本文中,我们将通过一个实际示例(构建一个图像压缩应用程序)来探讨 Web Worker 是什么、它们为何如此有用以及如何使用它们。是不是感觉很刺激?那就开始吧。
什么是 Web Worker?
Web Workers 允许 JavaScript 在后台运行任务,而不会阻塞主线程,从而保持 UI 的流畅性和响应速度。您可以使用 Web Workers API 创建它们,它接受两个参数:url
和options
。以下是创建 Web Workers 的一个简单示例。
const worker = new Worker('./worker.js', { type: 'module' });
为什么要使用 Web Worker?
如前所述,Web Worker 在后台运行任务。以下是使用它们的几个理由
-
防止在繁重计算期间页面滞后
-
高效处理大数据
-
提高复杂 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
为了在浏览器中压缩图像,我们将使用@jsquash/web
npm 库来编码和解码 WebP 图像。该库由 WebAssembly 提供支持,因此我们先安装它。
npm install @jsquash/webp
太好了,我们的项目设置完成了。在下一节中,我们将创建一个工作脚本来管理图像压缩。
创建工作者脚本
工作者脚本是一个 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 {};
在这里,我们从库中导入encode
和方法,并使用工作者全局范围来监听来自主线程的消息。decode
@jsquash/webp
self
当消息到达时,我们会获取图像文件和选项,然后通过先解码再使用质量选项进行编码来压缩图像。最后,我们使用postMessage
将压缩后的图像 blob 发送回主线程。如果出现错误,我们会处理错误并使用 发送错误消息postMessage
。
工作脚本已准备就绪。在下一节中,我们将构建 Imagelist 组件、更新样式、更新页面,并使用工作脚本处理压缩。
使用 Web Worker
在开始之前,让我们global.css
使用以下内容更新文件并删除默认样式。
@tailwind base;
@tailwind components;
@tailwind utilities;
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;
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>
);
}
首先,我们导入钩子和 ImageList 组件,以及 ImgData 类型。
import { useState, useRef, useEffect } from 'react';
import ImageList, { ImgData } from '../components/ImageList';
然后,我们创建一个 ref 来保存 Worker 实例,因为我们不想在每次重新渲染时重复创建 Worker。我们也希望避免在 Worker 实例发生变化时重新渲染组件。
const workerRef = useRef<Worker | null>(null);
在 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();
};
}, []);
为了管理图片文件的上传,我们使用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 },
});
});
};
最后,渲染元素文本区域、图像输入和图像列表。
-
用户选择图像:用户使用文件输入选择图像,这使得组件创建对象 URL 并将每个图像标记为“压缩”。
-
工作者通信:组件将每个图像文件(带有选项)发送到 Web 工作者。
-
并行进程:
- 文本区域交互:同时用户可以在文本区域进行输入,体现UI不被遮挡。
- 图像压缩:工作人员在后台压缩图像。
-
完成:压缩完成后,工作者将结果发送回组件,组件使用压缩图像更新 UI,同时文本区域保持平稳运行。
太好了,一切设置完毕。下一节,我们将运行该应用程序,并了解 Web Worker 的工作原理。
运行示例
打开终端并运行以下命令,然后转到http://localhost:3000/。
npm run dev
在此处尝试现场演示:https://web-worker-with-example.vercel.app/
结论
Web Workers 是提升应用程序性能的绝佳工具。使用 Web Workers,您可以确保应用程序运行更快、更流畅、响应更灵敏。但是,不应过度使用,仅在必要时使用。此外,请检查浏览器支持情况,目前全球浏览器支持率约为 98%。您可以点击此处查看。
本期内容就到这里。感谢您的阅读!如果您觉得本文有帮助,请点赞、评论并与他人分享。