Wyobraz sobie, ze na Twoim statku kosmicznym glowny komputer pokladowy (main thread) musi jednoczesnie renderowac kokpit, obslugiwac systemy nawigacyjne i przeliczac trajektorie lotu. Jesli obliczenia trajektorii sa zbyt ciezkie, kokpit zamraza sie - kapitan nie moze kliknac zadnego przycisku. Rozwiazanie? Dedykowany komputer obliczeniowy (Web Worker), ktory pracuje rownolegle, nie blokujac glownego systemu.
Web Workers to mechanizm przegladarki, ktory pozwala uruchamiac kod JavaScript w oddzielnym watku (thread). Glowny watek (main thread) odpowiada za rendering UI, obsluge eventow i interakcje uzytkownika. Gdy wykonujesz ciezkie obliczenia na main thread, interfejs sie "zamraza".
1// BEZ Web Worker - blokuje main thread
2function HeavyCalculation() {
3 const [result, setResult] = useState(null);
4
5 const calculate = () => {
6 // To zablokuje UI na kilka sekund!
7 let sum = 0;
8 for (let i = 0; i < 1_000_000_000; i++) {
9 sum += Math.sqrt(i);
10 }
11 setResult(sum);
12 };
13
14 return (
15 <div>
16 <button onClick={calculate}>Oblicz</button>
17 <p>Wynik: {result}</p>
18 </div>
19 );
20}Web Worker to oddzielny plik JavaScript, ktory komunikuje sie z glownym watkiem przez system wiadomosci (
postMessage / onmessage):1// worker.js - plik Workera (oddzielny watek)
2self.onmessage = function(event) {
3 const { type, data } = event.data;
4
5 if (type === 'CALCULATE_TRAJECTORY') {
6 const result = calculateTrajectory(data);
7 self.postMessage({ type: 'RESULT', result });
8 }
9};
10
11function calculateTrajectory(params) {
12 let trajectory = [];
13 for (let t = 0; t < params.steps; t++) {
14 const x = params.speed * t * Math.cos(params.angle);
15 const y = params.speed * t * Math.sin(params.angle) - 0.5 * 9.81 * t * t;
16 trajectory.push({ x, y, t });
17 }
18 return trajectory;
19}1// App.jsx - komponent React (glowny watek)
2import { useState, useRef, useEffect } from 'react';
3
4function TrajectoryCalculator() {
5 const [result, setResult] = useState(null);
6 const [isCalculating, setIsCalculating] = useState(false);
7 const workerRef = useRef(null);
8
9 useEffect(() => {
10 workerRef.current = new Worker(new URL('./worker.js', import.meta.url));
11
12 workerRef.current.onmessage = (event) => {
13 if (event.data.type === 'RESULT') {
14 setResult(event.data.result);
15 setIsCalculating(false);
16 }
17 };
18
19 return () => workerRef.current?.terminate();
20 }, []);
21
22 const handleCalculate = () => {
23 setIsCalculating(true);
24 workerRef.current.postMessage({
25 type: 'CALCULATE_TRAJECTORY',
26 data: { speed: 1000, angle: 0.785, steps: 100000 }
27 });
28 };
29
30 return (
31 <div>
32 <button onClick={handleCalculate} disabled={isCalculating}>
33 {isCalculating ? 'Obliczam...' : 'Oblicz trajektorie'}
34 </button>
35 {result && <p>Punktow trajektorii: {result.length}</p>}
36 </div>
37 );
38}Aby uniknac powtarzania logiki tworzenia i zarzadzania Workerami, mozemy stworzyc reusable hook:
1import { useState, useRef, useEffect, useCallback } from 'react';
2
3function useWorker(workerFactory) {
4 const [result, setResult] = useState(null);
5 const [error, setError] = useState(null);
6 const [isRunning, setIsRunning] = useState(false);
7 const workerRef = useRef(null);
8
9 useEffect(() => {
10 workerRef.current = workerFactory();
11
12 workerRef.current.onmessage = (event) => {
13 setResult(event.data);
14 setIsRunning(false);
15 };
16
17 workerRef.current.onerror = (err) => {
18 setError(err.message);
19 setIsRunning(false);
20 };
21
22 return () => workerRef.current?.terminate();
23 }, []);
24
25 const run = useCallback((data) => {
26 setIsRunning(true);
27 setError(null);
28 workerRef.current?.postMessage(data);
29 }, []);
30
31 return { result, error, isRunning, run };
32}Biblioteka Comlink (od Google Chrome Labs) pozwala traktowac Web Workers jak zwykle obiekty z async metodami:
1// worker.js z Comlink
2import * as Comlink from 'comlink';
3
4const navigationComputer = {
5 async calculateRoute(origin, destination) {
6 const route = computeOptimalPath(origin, destination);
7 return route;
8 },
9 async analyzeStarField(stars) {
10 return stars.map(star => ({
11 ...star,
12 habitableZone: calculateHabitableZone(star.mass, star.luminosity)
13 }));
14 }
15};
16
17Comlink.expose(navigationComputer);1// App.jsx z Comlink
2import * as Comlink from 'comlink';
3
4function NavigationPanel() {
5 const [route, setRoute] = useState(null);
6 const navComputer = useRef(null);
7
8 useEffect(() => {
9 const worker = new Worker(new URL('./worker.js', import.meta.url));
10 navComputer.current = Comlink.wrap(worker);
11 return () => worker.terminate();
12 }, []);
13
14 const planRoute = async () => {
15 // Wywolujemy metode jak zwykla funkcje async!
16 const result = await navComputer.current.calculateRoute(
17 { x: 0, y: 0, z: 0 },
18 { x: 4.2, y: 1.3, z: 0.7 }
19 );
20 setRoute(result);
21 };
22
23 return (
24 <div>
25 <button onClick={planRoute}>Planuj trase</button>
26 {route && <RouteVisualization data={route} />}
27 </div>
28 );
29}Web Workers sa idealne do:
Web Workers NIE sa potrzebne do:
postMessage / onmessageuseWorker - enkapsulacja logiki Workera