在最后一个模块中,我们简要介绍了 Web Worker。Web Worker 可以将 JavaScript 从主线程移到单独的 Web Worker 线程,从而提高输入响应速度,这有助于在您有不需要直接访问主线程的工作时,提高网站的与下次绘制的互动 (INP)。不过,仅有概览是不够的,本模块中提供了一个 Web Worker 的具体使用情形。
一个这样的使用情形可能是某个网站需要从图片中剥离 Exif 元数据,这并非天方夜谭。事实上,Flickr 等网站为用户提供了一种查看 Exif 元数据的方式,以便了解其托管图片的颜色深度、相机品牌和型号等技术细节以及其他数据。
不过,如果完全在主线程上执行提取图片、将其转换为 ArrayBuffer
和提取 Exif 元数据的逻辑,可能会非常耗费资源。幸运的是,Web 工作人员作用域允许在主线程之外完成此工作。然后,使用 Web Worker 的消息传递流水线,将 Exif 元数据作为 HTML 字符串传输回主线程,并显示给用户。
没有 Web Worker 时主线程的样貌
首先,观察在不使用 Web Worker 的情况下执行此工作时主线程的运行情况。为此,请执行以下步骤:
- 在 Chrome 中打开一个新标签页,然后打开其开发者工具。
- 打开效果面板。
- 前往 https://chrome.dev/learn-performance-exif-worker/without-worker.html。
- 在“性能”面板中,点击开发者工具窗格右上角的记录。
- 将此图片链接(或您选择的包含 Exif 元数据的其他链接)粘贴到相应字段中,然后点击 Get that JPEG! 按钮。
- 当界面填充了 Exif 元数据后,再次点击录制以停止录制。

请注意,除了可能存在的其他线程(例如光栅化器线程等)之外,应用中的所有内容都在主线程上发生。在主线程中,会发生以下情况:
- 该表单会获取输入内容,并调度
fetch
请求以获取包含 Exif 元数据的图像的初始部分。 - 图片数据会转换为
ArrayBuffer
。 exif-reader
脚本用于从图片中提取 Exif 元数据。- 系统会抓取元数据以构建 HTML 字符串,然后该字符串会填充元数据查看器。
现在,我们来对比一下实现相同行为(但使用 Web Worker)的实现!
使用 Web Worker 时主线程的样貌
现在,您已经了解了在主线程上从 JPEG 文件中提取 Exif 元数据的样子,接下来看看当 Web Worker 参与其中时会是什么样子:
- 在 Chrome 中打开另一个标签页,然后打开其开发者工具。
- 打开效果面板。
- 前往 https://chrome.dev/learn-performance-exif-worker/with-worker.html。
- 在“性能”面板中,点击开发者工具窗格右上角的“录制”按钮。
- 将此图片链接粘贴到相应字段中,然后点击 Get that JPEG! 按钮。
- 当界面填充了 Exif 元数据后,再次点击录制按钮以停止录制。

这就是 Web Worker 的强大之处。现在,除了使用 HTML 填充元数据查看器之外,所有操作都在单独的线程中完成,而不是在主线程中完成所有操作。这意味着主线程可以空出来执行其他工作。
或许,这里最大的优势在于,与不使用 Web Worker 的应用版本不同,exif-reader
脚本不是在主线程上加载的,而是在 Web Worker 线程上加载的。这意味着下载、解析和编译 exif-reader
脚本的费用是在主线程之外产生的。
现在,让我们深入了解使这一切成为可能的 Web Worker 代码!
Web Worker 代码概览
仅仅了解 Web Worker 的作用是不够的,还应了解(至少在本例中)该代码的结构,以便知道在 Web Worker 范围内可以实现哪些功能。
首先,编写需要在 Web Worker 出现之前执行的主线程代码:
// scripts.js
// Register the Exif reader web worker:
const exifWorker = new Worker('/js/with-worker/exif-worker.js');
// We have to send image requests through this proxy due to CORS limitations:
const imageFetchPrefix = 'https://res.cloudinary.com/demo/image/fetch/';
// Necessary elements we need to select:
const imageFetchPanel = document.getElementById('image-fetch');
const imageExifDataPanel = document.getElementById('image-exif-data');
const exifDataPanel = document.getElementById('exif-data');
const imageInput = document.getElementById('image-url');
// What to do when the form is submitted.
document.getElementById('image-form').addEventListener('submit', event => {
// Don't let the form submit by default:
event.preventDefault();
// Send the image URL to the web worker on submit:
exifWorker.postMessage(`${imageFetchPrefix}${imageInput.value}`);
});
// This listens for the Exif metadata to come back from the web worker:
exifWorker.addEventListener('message', ({ data }) => {
// This populates the Exif metadata viewer:
exifDataPanel.innerHTML = data.message;
imageFetchPanel.style.display = 'none';
imageExifDataPanel.style.display = 'block';
});
此代码在主线程上运行,并设置表单以将图片网址发送到 Web Worker。从这里开始,Web Worker 代码以加载外部 exif-reader
脚本的 importScripts
语句开头,然后设置与主线程的消息传递流水线:
// exif-worker.js
// Import the exif-reader script:
importScripts('/js/with-worker/exifreader.js');
// Set up a messaging pipeline to send the Exif data to the `window`:
self.addEventListener('message', ({ data }) => {
getExifDataFromImage(data).then(status => {
self.postMessage(status);
});
});
此 JavaScript 代码段用于设置消息传递流水线,以便当用户提交包含 JPEG 文件网址的表单时,该网址会到达 Web Worker。接下来,这段代码会从 JPEG 文件中提取 Exif 元数据,构建一个 HTML 字符串,然后将该 HTML 发送回 window
以最终显示给用户:
// Takes a blob to transform the image data into an `ArrayBuffer`:
// NOTE: these promises are simplified for readability, and don't include
// rejections on failures. Check out the complete web worker code:
// https://chrome.dev/learn-performance-exif-worker/js/with-worker/exif-worker.js
const readBlobAsArrayBuffer = blob => new Promise(resolve => {
const reader = new FileReader();
reader.onload = () => {
resolve(reader.result);
};
reader.readAsArrayBuffer(blob);
});
// Takes the Exif metadata and converts it to a markup string to
// display in the Exif metadata viewer in the DOM:
const exifToMarkup = exif => Object.entries(exif).map(([exifNode, exifData]) => {
return `
<details>
<summary>
<h2>${exifNode}</h2>
</summary>
<p>${exifNode === 'base64' ? `<img src="data:image/jpeg;base64,${exifData}">` : typeof exifData.value === 'undefined' ? exifData : exifData.description || exifData.value}</p>
</details>
`;
}).join('');
// Fetches a partial image and gets its Exif data
const getExifDataFromImage = imageUrl => new Promise(resolve => {
fetch(imageUrl, {
headers: {
// Use a range request to only download the first 64 KiB of an image.
// This ensures bandwidth isn't wasted by downloading what may be a huge
// JPEG file when all that's needed is the metadata.
'Range': `bytes=0-${2 ** 10 * 64}`
}
}).then(response => {
if (response.ok) {
return response.clone().blob();
}
}).then(responseBlob => {
readBlobAsArrayBuffer(responseBlob).then(arrayBuffer => {
const tags = ExifReader.load(arrayBuffer, {
expanded: true
});
resolve({
status: true,
message: Object.values(tags).map(tag => exifToMarkup(tag)).join('')
});
});
});
});
虽然内容有点多,但这也是一个相当复杂的 Web 工作器用例。不过,这些努力是值得的,而且不仅限于此使用情形。
您可以将 Web 工作器用于各种用途,例如隔离 fetch
调用和处理响应、处理大量数据而不阻塞主线程,而这仅仅是开始。
在提升 Web 应用的性能时,首先要考虑在 Web Worker 上下文中可以合理完成的任何操作。这些增益可能非常显著,有助于全面提升网站的用户体验。