מה זה Node.js ואיך הוא עובד מאחורי הקלעים

סער טויטו

סער טויטו

Software & Web Development


לפני שנתחיל ללמוד על Node.js, נשאל שאלה. מה חלק מ-JavaScript?

נענה על השאלה בהמשך המאמר.

מה זה Node.js?

Node.js היא סביבת הרצה של JavaScript, המבוססת על מנוע JavaScript V8 של Google. היא משמשת בעיקר להרצת קוד JavaScript בצד השרת, מה שהופך אותה לכלי מרכזי בפיתוח יישומים מבוססי שרת.

Node.js פועלת במודל event-driven עם single thread ו-non-blocking I/O. המשמעות היא שיש לה thread יחיד שמבצע את המשימות, אך היא יכולה לטפל במספר בקשות במקביל. זאת מכיוון שפעולות אסינכרוניות אינן חוסמות את הרצת התוכנה, מה שמאפשר יעילות גבוהה בטיפול בבקשות.

סביבת הרצה - Runtime environment

הכוונה בסביבת הרצה היא המקום שבו אנחנו מריצים קוד מסוים. כל סביבת הרצה היא שונה ותספק לנו פונקציות ויכולות מיוחדות בהתאם לסביבה בה אנחנו כותבים את הקוד.

לדוגמה, Chrome היא גם סביבת הרצה כמו Node.js, שנבנתה על גבי V8 ומאפשרת לנו להריץ קוד ולגשת ל-DOM API's, או לבצע button click events. אבל ב-Node.js אין תמיכה טבעית (built-in) ב-DOM API's לדוגמה, אבל איך זה אם שניהם בנויים על אותה טכנולוגיה (V8 JavaScript engine)?

V8 JavaScript engine

המנוע V8 JavaScript engine, שנכתב בשפת ++C, מיועד לקחת קוד JavaScript ולהמיר אותו לקוד מכונה, כך שהמחשב יוכל להריץ אותו. גם Node.js וגם Chrome מבוססים על מנוע V8, כלומר, שניהם נבנו בעיקר בעזרת ++C, מה שמאפשר לכל אחת מהן לספק פונקציות ויכולות שונות בהתאם לסביבת ההרצה שלה.

הייחודיות של V8 טמונה בכך שמי שמבין בשפת ++C יכול לשלב את מנוע ה-JavaScript הזה באפליקציות משלו. כך ניתן ליצור סביבת הרצה מותאמת אישית ולהרחיב את היכולות של JavaScript בהתאם לצרכים הספציפיים של היישום.

Andrew Mead, Rob Percival

בתמונה תוכלו לראות את שתי סביבות ההרצה שדיברנו עליהן (Node.js & Chrome) ואיך V8 JavaScript engine לוקח קוד JavaScript, וממיר אותו ל-machine code כך שהמחשב שלכם יוכל לבצע. שימו לב כי פונקציות שחשבתם שהם של Javascript הם בעצם Implementation של ++C, וכדי שנוכל לגשת אליהן, סביבת ההרצה מספק לנו את שכבה כמו Intermediate language שמאפשרת לנו לגשת אל אותן API's.

אם כך, נחזור לשאלה שלנו, מה חלק מ-JavaScript?

  • DOM
  • LocalStorage
  • setTimeout
  • console.log

התשובה כבר לא צריכה להפתיע אתכם, אבל אף אחד מהאפשרויות לא באמת חלק מ-JavaScript, זה רק ה-Implementation של סביבת ההרצה V8 JavaScript engine שנכתבה על ידי ++C שנותנת לנו לגשת ולבצע את הפעולות האלו של ה-API's דרך JavaScript.

איך Node.js עובד?

עכשיו הגענו לחלק השני של הפסקה שהוא: Node.js היא event-driven(single thread), non-blocking I/O model. כדי להקל על ההסבר, גם את המשפט הזה נחלק לשלושה חלקים:

  1. קלט/פלט - I/O Model (input/output)

    I/O, או במלואו Input/Output (קלט/פלט), הוא תהליך התקשורת של המחשב עם מערכות אחרות. לדוגמה, קריאת קובץ מהדיסק הקשיח, שליחת נתונים דרך הרשת וגם פעולות אסינכרוניות, כמו לשאוב מידע מ-APIs, הן דוגמא לפעולות I/O.

  2. Non-blocking

    במודל non-blocking, פעולות אסינכרוניות כמו שאיבת מידע מ-APIs יכולות לקחת זמן (נניח, 5 שניות). במהלך זמן הבקשה, אנחנו רוצים שהתוכנה שלנו תמשיך לעבוד ולא תיתקע. אם Node.js לא היה מיישם יכולת non-blocking, המחשב שלנו היה 'קופא' ולא יכול היה לבצע פעולות אחרות עד לסיום אותה בקשה.

    לכן, מודל ה-non-blocking אומר שאנחנו יכולים לבצע פעולות אסינכרוניות מבלי לחשוש שהתוכנה שלנו תיתקע, וכך Node.js מאפשר לנו לטפל בבקשות האלו בצורה יעילה ומיטבית.

Event-driven (single thread)

זהו המנגנון שבאמצעותו Node.js מנהל פעולות אסינכרוניות בצורה יעילה, גם כשהוא רץ על תהליכון אחד (thread יחיד). כל פעולה מתוזמנת להתבצע כשיגיע תורה, בלי לחסום את שאר הקוד. הנה החלקים שמרכיבים את המנגנון הזה:

  1. Call Stack

    זהו המקום שבו מתבצע הקוד הרגיל שלכם (קוד סינכרוני) — פונקציות, לולאות, תנאים ועוד. כל פעולה נכנסת לראש ה-Stack, וברגע שהיא מסתיימת, היא מוסרת ממנו. ברגע שה-Stack מתרוקן, המערכת פנויה להפעיל פעולות נוספות.

  2. Node APIs ו-libuv

    כאשר אתם כותבים קוד אסינכרוני שמשתמש בפונקציות כמו setTimeout, fs.readFile או http.get, חשוב להבין שאלו לא פונקציות של JavaScript עצמה — אלא של Node.js. הפונקציות האלו מגיעות מ-Node APIs, שהם אוסף של פונקציות שמובנות בתוך Node.js, ונכתבו ב-C++ מעל ספרייה פנימית בשם libuv.

    libuv אחראית לטפל בפעולות אסינכרוניות שמגיעות מ-Node APIs — לדוגמה, קריאת קבצים, בקשות HTTP או טיימרים. לעומת זאת, פונקציות סינכרוניות מתוך Node APIs, כמו fs.readFileSync, רצות ישירות ב-thread הראשי של JavaScript, כלומר ב-Call Stack בלי מעורבות של libuv.

    אם הפעולה "כבדה" כמו גישה לדיסק, libuv מפעיל thread pool (קבוצת תהליכונים שפועלים במקביל) כדי לבצע אותה מאחורי הקלעים. כאשר הפעולה מסתיימת, libuv לא מריץ את הקוד שלך מיד – אלא שולח את הפונקציה (ה-callback) לתור ממתינים בשם Callback Queue, שם היא מחכה לביצוע.

    חשוב להבין שלא כל פעולה אסינכרונית משתמשת ב-libuv. פעולות כמו Promises (למשל then או catch) מקורן ישירות ב-JavaScript ולכן מנוהלות ישירות על ידי מנוע ה-JavaScript (V8), ולא עוברות דרך Node APIs או thread pool כלל. הן נכנסות לתור נפרד הנקרא Microtask Queue.

  3. Callback Queue (Macrotask Queue)

    Callback Queue (לעיתים נקרא גם Macrotask Queue) הוא תור שמכיל callbacks של פעולות אסינכרוניות כמו setTimeout, fs.readFile, או בקשות רשת. אחרי שהן הסתיימו (בין אם דרך libuv או מנגנונים אחרים של Node APIs), הן מוכנסות לתור הזה.

  4. Microtask Queue

    תור נוסף שמכיל callbacks של פעולות אסינכרוניות עם עדיפות גבוהה יותר, כמו Promise.then, catch או finally. תור זה נקרא Microtask Queue, והוא מנוהל ישירות על ידי מנוע ה-JavaScript (V8), ללא תלות ב-libuv או Node APIs.

    לדוגמה: אם אתם מבצעים בקשת רשת עם fetch, החלק שמטפל בתגובה (כלומר, הקוד בתוך then) לא נכנס ל-Callback Queue כמו שאר הפעולות האסינכרוניות – אלא ל-Microtask Queue כיוון ש-then הוא חלק ממנגנון ה-Promise, שמנוהל ישירות על ידי מנוע ה-JavaScript (V8), ולכן יש לו עדיפות וכמובן שהוא לא יעבור דרך libuv.

  5. Event Loop

    זהו המנגנון שמחליט מה לבצע בכל רגע. הוא בודק כל הזמן אם ה-Call Stack פנוי, ואם כן — הוא מפעיל את מה שממתין בתורים.

    הסדר הוא כזה:

    1. בודק אם ה-Call Stack ריק
    2. אם כן, מבצע את כל המשימות בתור של ה-Microtask
    3. רק לאחר מכן עובר לתור של ה-Callback Queue ומבצע ממנו פעולה אחת או יותר
    4. אם בזמן הזה נוספו משימות חדשות ל-Microtask Queue, הוא חוזר לטפל בהן לפני שממשיך
    1) console.log("start"); // Call stack
    2) setTimeout(() => console.log("timeout"), 0); // Callback queue
    3) Promise.resolve().then(() => console.log("promise")); // Microtask queue
    4) console.log("end"); // Call stack

    Output:
    start
    end
    promise
    timeout

    כך נוצר תיאום בין הקוד שלך לבין כל הפעולות האסינכרוניות, והכול קורה בסדר ברור ויעיל.

Single Thread

single thread אומר שיש thread (חייל) אחד שמטפל במגוון משימות, כמו לנהל בקשות HTTP, להריץ פונקציות JavaScript ועוד. זה שונה מ-multiple threads שזה אומר שיש הרבה חיילים שיכולים לבצע את המשימות האלו במקביל.

ל-Node.js יש חייל אחד שמטפל בכל המשימות האלו וזה נשמע כאילו זה יכול להיות חיסרון, אבל במציאות, זה יתרון גדול. מכיוון שהוא עובד לבד, הוא יודע לעבוד בצורה יעילה מאוד, לחסוך במשאבי זיכרון ומעבד ומפחית את הצורך בתיאום עם threads (חיילים) אחרים מה שעוזר לנו לכתוב קוד פשוט יותר.

אך כמו שאמרנו, Node.js משתמש בספרייה בשם libuv שמאפשר ל-single thread של Node.js לבצע פעולות אסינכרוניות מבלי לחסום את ה-single thread שלנו - כך שהקוד שלך ממשיך לרוץ בלי להתעכב.

למה כדי לנו להשתמש ב-Node.js?

ל-Node.js יש יותר מכמה סיבות למה להשתמש בו, אנחנו נחקור את הבולטות ביותר:

  • סקלוביליות גבוהה (Highly scalable)

    ל-Node.js יש את היכולות להתמודד עם כמה משימות במקביל ולא להיתקע (non-blocking), מה שאומר שבעזרת Node.js אנחנו יכולים לטפל ביותר לקוחות ולתת תשומת לב לכולם. העובדה הזו, ביחד עם event-driven מאפשרים ל-Node.js להיות גמיש ומותאם ליישומים עם עומסי עבודה גבוהים של I/O, כמו שרתי API, מערכות streaming, ויישומים שמבצעים המון בקשות אסינכרוניות.

  • עומסי נתונים (Data-intensive)

    ל-Node.js יש את היכולות לנהל ולהתמודד עם כמות מידע (תהליכים גדולים וארוכים) בצורה יעילה שלא תוקעת את המערכת (thread) רק על אותה משימה ספציפית, אלא בזמן שהבקשה מעובדת, Node.js זמין לקבל בקשות נוספות, מה שאומר שה-thread זמין. כאשר התגובה מהמשימה המקורית מגיעה, היא מועברת ל-Callback Queue. זה מאפשר ניצול מקסימלי של המערכת.

  • יישומים בזמן אמת (Real-time applications)

    Node.js מותאם במיוחד ליישומים בזמן אמת (כמו WhatsApp), שבהם נדרשים שינויים מיידים ותגובות מהירות. בפועל, Node.js מאפשר תקשורת דו-כיוונית ומסוגל להשתלב עם WebSockets, מה שמקל על יצירת תקשורת בזמן אמת.

  • JavaScript ל-Backend ו-Frontend

    השימוש ב-Node.js מאפשר למתכנתים להשתמש ב-JavaScript גם בצד השרת וגם בצד הלקוח, מה שמאפשר למתכנתים לתכנת Full Stack Web Applications בשפה אחת!

  • פופולריות וקהילה תומכת

    Node.js נהנית מפופולריות גבוהה וקהילה גדולה של מפתחים, מה שמאפשר גישה רחבה למשאבים, ספריות ותמיכה מכל העולם! בנוסף מלא חברות גדולות כמו PayPal מספרות עד כמה Node.js עזרו להם:

    • שיפור ביצועי היישום: בזכות Node.js הצליחה PayPal לטפל בבקשות פי 2x לעומת הגרסה ב-Java.

    • הפחתת זמן התגובה: המעבר ל-Node.js הוביל להפחתה של 35% בזמן התגובה הממוצע, מה שהביא לחוויית משתמש מהירה יותר.

    • הפחתת מספר שורות קוד: במהלך המעבר ל-Node.js, יישמה PayPal את אותו פונקציונליות שהייתה להם ב-Java, אך עם 33% פחות שורות קוד. זו הפחתה משמעותית בכמות הקוד הדרושה, המעידה על יעילות גבוהה והקלות בפיתוח ובתחזוקה.

אבל לא רק PayPal, נכון לשנת 2024, מספר חברות מובילות משתמשות ב-Node.js בבאקנד שלהן, והנה כמה דוגמאות:

חברות שמשתמשות ב-Node.js בבאקנד שלהן, לשנת 2024:

  • Uber משתמשת ב-Node.js לטיפול במספר עצום של נתונים ובקשות משתמש בזמן אמת, מה שמאפשר לה לשמור על מהירות ויעילות בפלטפורמה הגדולה והמורכבת שלה.

  • NASA עברה ל-Node.js ושיפרה באופן משמעותי את זמני הגישה לנתונים ואת ניהול הנתונים בענן, מדגימה את יכולת של Node.js לטפל ביישומים קריטיים ובקנה מידה גדול.

  • LinkedIn & Trello שילבו את Node.js כדי להשיג יכולת עיבוד נתונים מהירה וביצועים גבוהים במערכות המבוססות על נתונים ומשימות בזמן אמת, שיפרו את זמינות השירות.

  • Netflix במקור השתמשה ב-Java בבאקנד שלה, החברה עברה ל-Node.js ושיפרה באופן משמעותי את זמן הטעינה של האפליקציה, קיצצה את זמן הטעינה ב-70%, ושיפרה את חוויית המשתמש.