Giới thiệu
JavaScript vốn được biết đến là một ngôn ngữ đơn luồng (single-threaded), chạy trên một main thread duy nhất. Điều này có thể gây ra các vấn đề về hiệu năng khi xử lý các tác vụ nặng, đặc biệt là trong các ứng dụng web phức tạp. Tuy nhiên, với sự ra đời của Web Workers và các API liên quan, JavaScript đã có thể tận dụng sức mạnh của lập trình đa luồng để cải thiện hiệu năng ứng dụng.
Trong bài viết này, chúng ta sẽ khám phá cách sử dụng Web Workers, Service Workers, và Worklets để tạo ra các ứng dụng web có khả năng xử lý đa luồng hiệu quả.
JavaScript và mô hình đơn luồng
Trước khi đi vào chi tiết về Web Workers, hãy hiểu tại sao JavaScript lại được thiết kế là đơn luồng và những hạn chế của mô hình này.
JavaScript chạy trên một “Event Loop” duy nhất, xử lý các tác vụ một cách tuần tự. Mô hình này đơn giản và dễ hiểu, nhưng có một nhược điểm lớn: nếu một tác vụ mất nhiều thời gian để hoàn thành, nó sẽ chặn toàn bộ UI và làm cho ứng dụng trở nên không phản hồi.
// Ví dụ về tác vụ nặng chặn main thread
function heavyCalculation() {
const iterations = 10000000000;
let result = 0;
for (let i = 0; i < iterations; i++) {
result += Math.sqrt(i);
}
return result;
}
console.log("Bắt đầu tính toán...");
const result = heavyCalculation(); // Sẽ chặn UI trong thời gian dài
console.log("Kết quả:", result);
console.log("Tính toán hoàn tất");
Trong ví dụ trên, trong khi hàm heavyCalculation()
đang chạy, người dùng không thể tương tác với trang web - không thể nhấp vào nút, cuộn trang, hoặc thực hiện bất kỳ hành động nào khác. Đây là một trải nghiệm người dùng tồi.
Web Workers: Giải pháp đa luồng cho JavaScript
Web Workers cung cấp một cách để chạy JavaScript trong background threads, tách biệt với main thread. Điều này cho phép bạn thực hiện các tác vụ nặng mà không làm ảnh hưởng đến UI và trải nghiệm người dùng.
Các loại Web Workers
- Dedicated Workers: Được sử dụng bởi một script duy nhất
- Shared Workers: Có thể được chia sẻ giữa nhiều scripts, thậm chí từ các origin khác nhau
- Service Workers: Hoạt động như proxy mạng giữa ứng dụng và server, cho phép offline access
Cách tạo và sử dụng Dedicated Worker
main.js (Main Thread)
console.log("Main thread: Khởi tạo worker");
// Tạo một worker mới
const worker = new Worker("worker.js");
// Gửi dữ liệu đến worker
worker.postMessage({
command: "calculate",
iterations: 1000000000,
});
// Nhận kết quả từ worker
worker.onmessage = function (event) {
console.log("Main thread: Nhận kết quả từ worker:", event.data);
};
// Xử lý lỗi
worker.onerror = function (error) {
console.error("Worker error:", error.message);
};
console.log("Main thread: UI vẫn phản hồi trong khi worker đang xử lý");
worker.js (Worker Thread)
// Lắng nghe message từ main thread
self.onmessage = function (event) {
console.log("Worker: Nhận yêu cầu từ main thread", event.data);
if (event.data.command === "calculate") {
const result = heavyCalculation(event.data.iterations);
// Gửi kết quả về main thread
self.postMessage(result);
}
};
function heavyCalculation(iterations) {
let result = 0;
for (let i = 0; i < iterations; i++) {
result += Math.sqrt(i);
}
return result;
}
Ưu điểm của Web Workers
- Không chặn UI: Các tác vụ nặng chạy trong background, giữ cho UI luôn phản hồi
- Tận dụng nhiều CPU cores: Cải thiện hiệu năng trên các thiết bị đa nhân
- Tách biệt mã: Tổ chức mã tốt hơn bằng cách tách logic xử lý nặng
Hạn chế của Web Workers
- Không có quyền truy cập vào DOM: Workers không thể trực tiếp thao tác với DOM
- Không có quyền truy cập vào các API window: Không thể sử dụng
window
,document
,parent
- Chi phí giao tiếp: Giao tiếp giữa main thread và worker có overhead
- Serialization: Dữ liệu phải được serialized khi truyền giữa threads
Transferable Objects và SharedArrayBuffer
Để giảm chi phí giao tiếp giữa main thread và workers, JavaScript cung cấp hai cơ chế:
Transferable Objects
Cho phép chuyển quyền sở hữu object thay vì sao chép, giúp tăng hiệu năng đáng kể khi làm việc với dữ liệu lớn.
// Main thread
const buffer = new ArrayBuffer(1024 * 1024 * 32); // 32MB buffer
worker.postMessage(buffer, [buffer]); // Chuyển quyền sở hữu buffer
// Sau khi transfer, buffer không còn sử dụng được ở main thread
console.log(buffer.byteLength); // 0
SharedArrayBuffer
Cho phép chia sẻ bộ nhớ giữa main thread và workers, giúp tránh việc sao chép dữ liệu.
// Main thread
const sharedBuffer = new SharedArrayBuffer(1024 * 1024 * 32); // 32MB shared buffer
const sharedArray = new Uint32Array(sharedBuffer);
worker.postMessage({ sharedArray });
// Worker thread
self.onmessage = function (event) {
const { sharedArray } = event.data;
// Cả main thread và worker đều có thể đọc/ghi vào sharedArray
Atomics.add(sharedArray, 0, 1); // Tăng giá trị tại index 0 lên 1
};
Lưu ý về bảo mật: Để sử dụng
SharedArrayBuffer
, bạn cần thiết lập các HTTP headers bảo mật:Cross-Origin-Opener-Policy: same-origin Cross-Origin-Embedder-Policy: require-corp
Service Workers: Web Workers đặc biệt
Service Workers là một loại Web Worker đặc biệt, hoạt động như proxy mạng giữa ứng dụng và server. Chúng cho phép:
- Offline access: Lưu trữ và phục vụ tài nguyên từ cache khi không có kết nối mạng
- Background sync: Đồng bộ dữ liệu khi có kết nối mạng trở lại
- Push notifications: Nhận và xử lý push notifications
Đăng ký Service Worker
if ("serviceWorker" in navigator) {
window.addEventListener("load", function () {
navigator.serviceWorker
.register("/service-worker.js")
.then(function (registration) {
console.log(
"Service Worker đã đăng ký thành công:",
registration.scope
);
})
.catch(function (error) {
console.log("Đăng ký Service Worker thất bại:", error);
});
});
}
Ví dụ Service Worker đơn giản
// service-worker.js
const CACHE_NAME = "my-site-cache-v1";
const urlsToCache = [
"/",
"/styles/main.css",
"/scripts/main.js",
"/images/logo.png",
];
// Cài đặt Service Worker và cache tài nguyên
self.addEventListener("install", function (event) {
event.waitUntil(
caches.open(CACHE_NAME).then(function (cache) {
console.log("Opened cache");
return cache.addAll(urlsToCache);
})
);
});
// Lắng nghe fetch requests
self.addEventListener("fetch", function (event) {
event.respondWith(
caches.match(event.request).then(function (response) {
// Trả về response từ cache nếu có
if (response) {
return response;
}
// Nếu không có trong cache, fetch từ network
return fetch(event.request).then(function (response) {
// Kiểm tra response hợp lệ
if (!response || response.status !== 200 || response.type !== "basic") {
return response;
}
// Clone response để cache
var responseToCache = response.clone();
caches.open(CACHE_NAME).then(function (cache) {
cache.put(event.request, responseToCache);
});
return response;
});
})
);
});
Worklets: Web Workers nhỏ gọn cho các tác vụ đặc biệt
Worklets là phiên bản nhẹ của Web Workers, được thiết kế cho các tác vụ đặc thù như rendering và audio processing.
Các loại Worklets
- Paint Worklet: Cho phép tạo custom painting effects trong CSS
- Layout Worklet: Cho phép tạo custom layout algorithms
- Animation Worklet: Cho phép tạo custom animations với hiệu năng cao
- Audio Worklet: Cho phép xử lý audio với độ trễ thấp
Ví dụ về Paint Worklet
HTML & CSS
<div class="custom-background"></div>
<style>
.custom-background {
width: 300px;
height: 300px;
background-image: paint(checkerboard);
}
</style>
JavaScript
// Đăng ký Paint Worklet
CSS.paintWorklet.addModule("checkerboard.js");
checkerboard.js (Paint Worklet)
class CheckerboardPainter {
static get inputProperties() {
return ["--checkerboard-size", "--checkerboard-color"];
}
paint(ctx, size, properties) {
// Lấy custom properties từ CSS hoặc sử dụng giá trị mặc định
const checkerboardSize =
parseInt(properties.get("--checkerboard-size").toString()) || 10;
const color = properties.get("--checkerboard-color").toString() || "black";
// Vẽ checkerboard pattern
for (let y = 0; y < size.height; y += checkerboardSize) {
for (let x = 0; x < size.width; x += checkerboardSize) {
if ((x / checkerboardSize + y / checkerboardSize) % 2 === 0) {
ctx.fillStyle = color;
ctx.fillRect(x, y, checkerboardSize, checkerboardSize);
}
}
}
}
}
// Đăng ký painter
registerPaint("checkerboard", CheckerboardPainter);
Các mẫu thiết kế và best practices
1. Worker Pool
Thay vì tạo và hủy workers liên tục, tạo một pool các workers tái sử dụng.
class WorkerPool {
constructor(size, workerScript) {
this.workers = [];
this.queue = [];
this.activeWorkers = new Map();
// Tạo pool workers
for (let i = 0; i < size; i++) {
const worker = new Worker(workerScript);
worker.onmessage = this.handleWorkerMessage.bind(this, worker);
this.workers.push(worker);
}
}
handleWorkerMessage(worker, event) {
// Lấy callback từ Map và gọi với kết quả
const callback = this.activeWorkers.get(worker);
if (callback) {
callback(null, event.data);
this.activeWorkers.delete(worker);
// Xử lý task tiếp theo trong queue nếu có
if (this.queue.length > 0) {
const { task, callback } = this.queue.shift();
this.runTask(worker, task, callback);
}
}
}
runTask(worker, task, callback) {
this.activeWorkers.set(worker, callback);
worker.postMessage(task);
}
exec(task, callback) {
// Tìm worker rảnh
const availableWorker = this.workers.find(
(w) => !this.activeWorkers.has(w)
);
if (availableWorker) {
this.runTask(availableWorker, task, callback);
} else {
// Thêm vào queue nếu không có worker rảnh
this.queue.push({ task, callback });
}
}
terminate() {
this.workers.forEach((w) => w.terminate());
this.workers = [];
this.queue = [];
this.activeWorkers.clear();
}
}
// Sử dụng
const pool = new WorkerPool(4, "worker.js");
pool.exec({ type: "calculate", data: [1, 2, 3] }, (err, result) => {
console.log("Kết quả:", result);
});
2. Comlink: Đơn giản hóa giao tiếp với Workers
Comlink là thư viện giúp đơn giản hóa giao tiếp giữa main thread và workers, cho phép bạn gọi functions từ worker như gọi functions thông thường.
// worker.js
import * as Comlink from "https://unpkg.com/comlink/dist/esm/comlink.mjs";
const api = {
calculateFibonacci(n) {
if (n <= 1) return n;
return this.calculateFibonacci(n - 1) + this.calculateFibonacci(n - 2);
},
async heavyTask(iterations) {
let result = 0;
for (let i = 0; i < iterations; i++) {
result += Math.sqrt(i);
}
return result;
},
};
Comlink.expose(api);
// main.js
import * as Comlink from "https://unpkg.com/comlink/dist/esm/comlink.mjs";
async function init() {
const worker = new Worker("worker.js", { type: "module" });
const api = Comlink.wrap(worker);
console.log("Calculating Fibonacci...");
const fibResult = await api.calculateFibonacci(40);
console.log("Fibonacci result:", fibResult);
console.log("Running heavy task...");
const heavyResult = await api.heavyTask(1000000000);
console.log("Heavy task result:", heavyResult);
}
init();
3. Workbox: Framework cho Service Workers
Workbox là bộ thư viện của Google giúp đơn giản hóa việc sử dụng Service Workers.
// service-worker.js
importScripts(
"https://storage.googleapis.com/workbox-cdn/releases/6.1.5/workbox-sw.js"
);
// Cấu hình cache
workbox.core.setCacheNameDetails({
prefix: "my-app",
suffix: "v1",
});
// Cache các tài nguyên tĩnh
workbox.routing.registerRoute(
({ request }) => request.destination === "image",
new workbox.strategies.CacheFirst({
cacheName: "images",
plugins: [
new workbox.expiration.ExpirationPlugin({
maxEntries: 60,
maxAgeSeconds: 30 * 24 * 60 * 60, // 30 ngày
}),
],
})
);
// Cache API requests với network-first strategy
workbox.routing.registerRoute(
({ url }) => url.pathname.startsWith("/api/"),
new workbox.strategies.NetworkFirst({
cacheName: "api-cache",
plugins: [
new workbox.expiration.ExpirationPlugin({
maxEntries: 50,
maxAgeSeconds: 5 * 60, // 5 phút
}),
],
})
);
// Precache và route
workbox.precaching.precacheAndRoute([
{ url: "/index.html", revision: "383676" },
{ url: "/styles/main.css", revision: "234234" },
{ url: "/scripts/main.js", revision: "987654" },
]);
Các ứng dụng thực tế
1. Xử lý hình ảnh
// main.js
const imageProcessingWorker = new Worker("image-processor.js");
function applyFilter(imageData, filter) {
return new Promise((resolve, reject) => {
imageProcessingWorker.onmessage = function (e) {
resolve(e.data);
};
imageProcessingWorker.onerror = reject;
imageProcessingWorker.postMessage({
imageData,
filter,
});
});
}
// Sử dụng
const canvas = document.getElementById("myCanvas");
const ctx = canvas.getContext("2d");
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
applyFilter(imageData, "grayscale")
.then((processedData) => {
ctx.putImageData(processedData, 0, 0);
console.log("Đã áp dụng filter thành công");
})
.catch((error) => {
console.error("Lỗi khi xử lý ảnh:", error);
});
// image-processor.js
self.onmessage = function (e) {
const { imageData, filter } = e.data;
// Clone imageData để xử lý
const data = new Uint8ClampedArray(imageData.data);
const width = imageData.width;
const height = imageData.height;
switch (filter) {
case "grayscale":
applyGrayscale(data);
break;
case "blur":
applyBlur(data, width, height);
break;
// Thêm các filter khác
}
// Gửi kết quả về main thread
self.postMessage(new ImageData(data, width, height));
};
function applyGrayscale(data) {
for (let i = 0; i < data.length; i += 4) {
const avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
data[i] = avg; // R
data[i + 1] = avg; // G
data[i + 2] = avg; // B
// data[i + 3] là alpha, giữ nguyên
}
}
function applyBlur(data, width, height) {
// Implement blur algorithm
// ...
}
2. Phân tích dữ liệu lớn
// main.js
const dataAnalysisWorker = new Worker("data-analyzer.js");
function analyzeData(dataset, options) {
return new Promise((resolve, reject) => {
dataAnalysisWorker.onmessage = function (e) {
resolve(e.data);
};
dataAnalysisWorker.onerror = reject;
dataAnalysisWorker.postMessage({
dataset,
options,
});
});
}
// Sử dụng
fetch("https://api.example.com/large-dataset")
.then((response) => response.json())
.then((dataset) => {
return analyzeData(dataset, {
groupBy: "region",
metrics: ["sales", "profit"],
timeRange: { start: "2023-01-01", end: "2023-12-31" },
});
})
.then((results) => {
console.log("Kết quả phân tích:", results);
renderChart(results);
})
.catch((error) => {
console.error("Lỗi khi phân tích dữ liệu:", error);
});
// data-analyzer.js
self.onmessage = function (e) {
const { dataset, options } = e.data;
console.log(
"Worker: Bắt đầu phân tích dữ liệu với",
dataset.length,
"bản ghi"
);
const results = analyzeDataset(dataset, options);
self.postMessage(results);
};
function analyzeDataset(dataset, options) {
// Implement data analysis logic
// ...
// Ví dụ: Nhóm và tính tổng theo region
const groupedData = {};
dataset.forEach((item) => {
if (isInTimeRange(item.date, options.timeRange)) {
const key = item[options.groupBy];
if (!groupedData[key]) {
groupedData[key] = {};
options.metrics.forEach((metric) => {
groupedData[key][metric] = 0;
});
}
options.metrics.forEach((metric) => {
groupedData[key][metric] += item[metric] || 0;
});
}
});
return groupedData;
}
function isInTimeRange(dateStr, range) {
const date = new Date(dateStr);
const start = new Date(range.start);
const end = new Date(range.end);
return date >= start && date <= end;
}
Kết luận
Web Workers và các API liên quan đã mở ra một kỷ nguyên mới cho JavaScript, cho phép tận dụng sức mạnh của lập trình đa luồng để xây dựng các ứng dụng web hiệu năng cao. Mặc dù có một số hạn chế, nhưng với các kỹ thuật và thư viện phù hợp, bạn có thể vượt qua những thách thức này và tạo ra trải nghiệm người dùng mượt mà hơn.
Khi xây dựng ứng dụng web phức tạp, hãy xem xét các tác vụ nào có thể được chuyển sang Web Workers để giải phóng main thread và cải thiện hiệu năng tổng thể. Đặc biệt, các tác vụ như xử lý hình ảnh, phân tích dữ liệu, mã hóa/giải mã, và các thuật toán phức tạp là những ứng viên lý tưởng để chuyển sang workers.
Với sự phát triển liên tục của các API web, chúng ta có thể mong đợi nhiều cải tiến hơn nữa trong tương lai để làm cho lập trình đa luồng trong JavaScript trở nên dễ dàng và mạnh mẽ hơn.