רוב הזמן יישומי Front End לא עושים עבודה חישובית יותר מדי קשה, ולכן את רוב שיפורי הביצועים בריאקט אפשר לפתור עם צמצום קריאות מהשרת או צמצום render-ים. אבל מדי פעם כן יש לנו קומפוננטה שצריכה לעשות עבודה חישובית, וכשזה קורה כדאי לדעת מה לעשות - והתשובה הפשוטה היא Web Worker.
ניקח לדוגמה את הקומפוננטה הבאה שמציגה את מספר המספרים הראשוניים עד מיליון:
import { useState } from 'react';
function isPrime(n) {
for (let i=2; i < n/2; i++) {
if (n % i === 0) {
return false;
}
}
return true;
}
function calculatePrimesUntilMillion() {
let count = 0;
console.log('Start');
for (let i=2; i < 1000000; i++) {
if (isPrime(i)) {
count += 1;
}
}
console.log('Ready');
return count;
}
function App() {
const [_, forceRender] = useState(0);
return (
<div className="App">
<p>There are {calculatePrimesUntilMillion()} prime numbers < 1,000,000</p>
<button onClick={() => forceRender(v => !v)}>Calculate again</button>
</div>
);
}
export default App;
אני כתבתי לבד את הקוד שסופר ואני אעשה לכם ספוילר - זאת ממש לא הדרך הכי יעילה לספור מספרים ראשוניים. למעשה היא מספיק לא יעילה כדי שאם תריצו את הקומפוננטה הזאת על המחשב תוכלו להרגיש כמה שניות טובות שהדפדפן ממש "תקוע", ואפילו לא מגיב כשמשנים את גודל החלון. אם יהיה לכם מזל תקבלו מכרום את ההודעה שהטאב נתקע והוא מציע לכם לסגור אותו.
כל זה קורה בגלל שלפונקציה calculatePrimesUntilMillion
לוקח יותר מדי זמן לרוץ, ובזמן הזה היא תוקעת את כל ממשק המשתמש. ברור שהדרך הכי טובה לפתור את הבעיה במקרה הזה היא לתקן את הקוד כך שירוץ יותר מהר, אבל במקרה הכללי לפעמים אתם באמת צריכים לעשות הרבה עבודה. בשביל זה בדיוק יש לנו את ה Web Worker.
ווב וורקר הוא דרך להפעיל קוד חישובי ב Thread אחר ולקבל ממנו תשובה. בריאקט אנחנו נשתמש ב useEffect כדי לשלוח הודעה ל Worker שלנו, ה Worker יעשה את החישוב בצורה אסינכרונית ובסוף נשתמש במשתנה State כדי לשמור את התוצאה ולהציג אותה. בעצם Web Worker יהפוך את הקוד שלנו מכזה ש"תוקע" את הדפדפן לקוד אסינכרוני רגיל שמחכה למשהו שיקרה.
אני משתמש ב create-react-app בגירסה החדשה ביותר והוא משתמש ב webpack 5 ושם התמיכה מ Web Worker היא מובנית. בשביל להפוך את הפרויקט שלי להשתמש ב Web Worker אני צריך:
- ליצור קובץ חדש בשם primes.js עם התוכן הבא:
function isPrime(n) {
for (let i=2; i < n/2; i++) {
if (n % i === 0) {
return false;
}
}
return true;
}
function calculatePrimesUntilMillion() {
let count = 0;
console.log('Start');
for (let i=2; i < 1000000; i++) {
if (isPrime(i)) {
count += 1;
postMessage({ count });
}
}
console.log('Ready');
return count;
}
onmessage = function(_ev) {
const count = calculatePrimesUntilMillion();
postMessage({ count });
}
- לעדכן את קוד הקומפוננטה כדי לטעון את ה Worker:
const worker = new Worker(new URL('./primes.js', import.meta.url));
לעדכן את הקומפוננטה עצמה כך שתשתמש ב Worker, כלומר תשלח לו הודעות ותקבל ממנו עדכונים:
function App() {
const [forceRender, setForceRender] = useState(0);
const [primesCount, setPrimesCount] = useState(0);
useEffect(() => {
worker.onmessage = function(ev) {
setPrimesCount(ev.data.count);
};
worker.postMessage({});
}, [forceRender]);
return (
<div className="App">
<p>There are {primesCount} prime numbers < 1,000,000</p>
<button onClick={() => setForceRender(v => !v)}>Calculate again</button>
</div>
);
}
בהפעלה בגירסה החדשה לא רק שהדפדפן ממשיך להגיב בזמן החישוב, אלא שאנחנו גם רואים את המספרים עולים - כי כל פעם שה Worker מוצא מספר ראשוני חדש הוא שולח Post Message שמגיע לקומפוננטה וגורם לעדכון המשתנה primesCount.
אם אתם עובדים בגירסאות ישנות יותר של create-react-app או וובפאק, שווה לבדוק את הספריה https://github.com/developit/workerize-loader שמאפשרת לטעון Web Worker גם בוובפאק 4.