47 ספריות ג'אווהסקריפט שכל מפתח *חייב* להכיר (חלק א')

האמת: לא באמת "חייב" – אבל כדאי. להכיר שהן קיימות ולדעת מה מרחב הכלים הקיים לפיתוח ג'אווהסקריפט.מתכנת שמגיע לג'אווהסקריפט מעולם ה NET. או JEE עשוי להרגיש קצת אבוד: בשתי פלטפורמות אלו (בעיקר NET.) המתכנת קיבל Stack שלם של טכנולוגיות קצה-לקצה שמתאימות לפתרון מגוון רחב מאוד של בעיות.

כשיש דילמה נוסח: "כיצד כותבים אפליקציות מסוג T ?" – פשוט הולכים לתיעוד של מייקרוסופט או סאן עורקל ולומדים מהי "הטכנולוגיה המומלצת" לצורך זה: ASP.NET? Entity Framework אולי JAAS או javaFaces? הכל (לכאורה) שם.

תמיד היה ניתן לחפש "מחוץ לקופסה" ולמצוא הרבה ספריות קוד-פתוח מעניינות – אך הספריות שהגיעו בחבילה היו "מספיק טובות".

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

מאיפה מוצאים את הספריות הללו? תחפשו!

כנראה שתגיעו מהר מאוד תוסיפו לארסנל שלכם את jQuery כמה פקדים ויזואליים וכלי Shim (אם אתם תומכים בדפדפנים ישנים).

מעבר לבסיס הזה יש עוד הרבה ספריות חשובות שכדאי להכיר.
אני לא מדבר על הספריות ל"כיבוי שרפות" נוסח "אני זקוק לפקד של תאריך" או "אנימציה של שעון מסתובב" – את אלו תמצאו בגוגל ברגע שיעלה הצורך. אני מדבר על ספריות שהן יותר לטווח הארוך – על מנת לכתוב ג'אווהסקריפט בצורה מסודרת ונכונה יותר. ספריות חשובות אך לא דחופות.

הכירו את JSter

הטריגר לפוסט הוא אתר נחמד שנתקלתי בו בשם JSter המרכז רשימה של ספריות ג'אווהסקריפט.
האם אין המון אתרים כאלה? – יש, אבל בג'סטר היו 2 דברים שקסמו לי:

  • הוא מנסה למיין את הספריות (בתוך הקטגוריות) ע"פ הפופולריות שלהן  – ולהציג קודם את הספריות החשובות ביותר.
  • הוא מוסיף לכל ספרייה את הלוגו שלה – תוספת קטנה, אבל חשובה, לטעמי. הכרת הסמלים עוזרת להתמצא ביתר קלות ולזהות מהר יותר ספריות שאתם "נתקלים בהן פעם נוספת". שמות הספריות רבות בג'ווהסקריפט דומה ומזכיר אחד את השני. למשל: EmbedJs, Ember.js ו emojify.js או sprite.js, spine.js, spry ו spin.js. כאלה. לכו תבדילו.
אם JSter קיים – מדוע צריך את הפוסט הזה?
  • לכלי אוטומטי יש הטיות וטעויות. למשל ג'סטר קבע ש SenchaTouch היא ספריית ה Mobile במקום ה 16 (מתוך 19), ממש לאחר ספרייה כלשהי בשם JindoMobile (??). זו שטות גמורה ודיי ברור ש Sencha היא השנייה הפופולארית אחרי jQueryMobile, אם לא הראשונה. מקור הטעות נובע כנראה מכך שעל ספריות שלא מנוהלות ב Github לג'סטר אין נתונים והוא מדרג אותן אחרונות / ע"פ הצבעות באתר בלבד.
  • רציתי להוסיף כמה הערות משלי. ישנן ספריות פופולאריות אך בירידה, ישנן ספריות חדשות אך מאוד מעניינות ויש כמה מילים שאפשר לכתוב על הספרייה שיוצר הספרייה לא כתב עליה.
כמה הערות נוספות:
  • כפי שציינתי, ניסיתי להתמקד בספריות "טווח ארוך" ולא כאלו שפותרות בעיה מיידית.
  • ניסיתי לקבץ את הספריות קצת אחרת מג'סטר בצורה שנראתה לי יותר הגיונית.
  • עדיין השתמשתי בצילומי מסך מג'סטר הכוללים נתונים שימוש מ github. ג'סטר יתעדכן עם הזמן, והפוסט הזה – לא.
  • יש כאן עניין של דעה אישית – שאחרים עלולים לא להסכים איתה. אני כבר מצפה לכמה תגובות בנוסח: "אבל איך לא ציינת את ?"
  • גם ל"כלי אנושי" יש הטיות וטעויות. ייתכן ויש ספריות שאני חושב שהן "לא פעילות" או "פאסה" – ואני פשוט לא יודע.
  • אני מציג בכל קטוגריה מספר ספריות שהן חלופות אחת לשנייה. אינני מנסה להמליץ ללמוד את כולן, חס ושלום. בעיקר נראה לי חשוב להכיר את האופציות השונות ותחומים בהן כל אופציה חזקה.
יאללה לדרך!

ספריות בסיס

jQuery היא ספריית "חובה" שבשימוש ע"י כמעט-כל מפתח ווב שאני מכיר. Zepto היא ספרייה המציגה API תואם ל jQuery – אבל עם כמה הבדלים:

  • Zepto, בהגדרה, תומכת רק בדפדפנים מודרניים. היא לא תומכת ב IE (אם כי תמיכה ב IE גרסה +10 נשקלת).
  • היא לא כוללת את כל הפונקציונליות, אלא קצת פחות. מה שקיים – תואם ל jQuery.
  • גודל הספרייה היה כשישית מ jQuery כשהספרייה הוצגה, כיום הוא כשליש.
מדוע להשתמש בספרייה שעושה את מה ש jQuery עושה, תומכת בפחות דפדפנים ובפחות פונקציות? בגלל הגודל.
קהל היעד של Zepto היה בעיקר אפליקציות / אתרים למובייל, היכן שגודל ה javaScript הוא משמעותי לביצועי האפליקציה.

דיי מהר הפכה Zeprto לאופציה פופולרית בעולם המובייל. "שליטה בעולם המובייל?!" – jQuery נלחצה, נבהלה, והגיבה בהתאם: התפצלה לגרסת "Legacy Support" (גרסאות 1.9) וגרסה רגילה (גרסאות +2) והוסיפה אופציה לוותר על פיצ'רים כדי להפחית את נפח ההורדה ולצמצם פערים מול Zepto (עדיין יש פער – אבל מפתחים רבים יוותרו עליו בכדי לזכות ב jQuery שהם מכירים). נחייה ונראה כיצד תחרות זו תמשיך ותתפתח.

Prototype ו MooTools היו שתי מתחרות של jQuery שלא עמדו בתחרות. אני לא מכיר אף אחד שמשתמש בהן כיום – אבל יש עדיין המון קוד כתוב שעובד איתן, בעיקר עם Prototype.
עוד על jQuery וחברותיה כתבתי בפוסט מבוא מואץ ל jQuery עבור מפתחי #C ו Java מנוסים.
לינק: השאווה בין הספריות (ע"פ Minified)

Frameworks בסיס

בניגוד ל jQuery, שהיא ספרייה (כלומר: הקוד שלנו קורא לה כשאנו מחליטים), ישנם גם Frameworks (כלומר: הם קוראים לקוד שלנו כשהם מחליטים). מטבעם Frameworks הם רחבים יותר מספריה והתלות בהם גדולה יותר: הם מכתיבים לנו איך לכתוב את הקוד אבל יכולים, מצד שני, לאפשר לנו להגיע לאפליקציה הרצויה בפחות קוד.

עוד הבדל מהותי הוא שבפיתוח עם jQuery המפתח עדיין עובד עם ה abstractions של הדפדפן, קרי Style Sheets, DOM וכו' – jQuery מספקת API נוחים יותר לעבודה עם מודל זה. עקרון זה נכון גם, פחות או יותר, עבור Polymer.
Enyo, YUI ו Dojo מספקים abstraction שהוא הרבה יותר Object Oriented ומבוסס javaScript – תמונת עולם שמשקפת פחות טוב את מה שקורה בדפדפן – ולכן כנראה קשה יותר לסגל אותה לחידושים בדפדפן.

הבדל מהותי אחרון הוא נושא ההיקף: אפשר לומר של jQuery ו YUI (או Dojo) יש חפיפה, אולם בעוד של 80% מהיכולות של jQuery יש תחליפים ב YUI/Dojo – אולי ל 30% מהיכולות של YUI/Dojo יש תחליפים ב jQuery. רשימת היכולות שיש ל YUI ואין ל jQuery כוללת: פקדי UI (מה שמכוסה חלקית ע"י jQuery UI), הרחבות לשפת ג'אווהסקריפט שמספקות מחלקות (בניגוד לאובייקטים, מה שנקרא Class System), ניהול state על ה URL, יכולות input validation, ועוד.

Dojo ו YUI הן דיי מקבילות אחת לשנייה בסט היכולות (שכולל הרבה מאוד), אם כי פילוסופיות המימוש שונות. נראה שמעמדן הולך ונשחק עם השנים לטובת jQuery עם השלמות. כלומר: במקום לבחור ב Framework אחד שעושה את רוב העבודה – מעדיפים להרכיב סט מותאם-אישית של ספריות קטנות יותר.
שתיהן ספריות שהשפיעו רבות על הרבה עולם הג'אווהסקריפט ורבות מהספריות החדשות שאנו רואים כיום הושפעו לקחו רעיונות משתי ספריות אלה. על כן, שמור להן מקום של כבוד בפנתאון של עולם הג'אווהסקריפט – לנצח (קרי כ-5 עד 10 שנים).
Polymer היא המאפה החם החדש מהתנור של גוגל שאמור לאפשר לנו להרחיב את שפת ה HTML לתגיות משלנו תוך שימוש בתקנים עתידיים כגון HTML Custom Elements, שימוש ב HTML Imports  – בכדי לשתף תגיות אלו ו Shadow DOM על מנת לקבל הפרדה בין קוד התגיות לשאר התוכנה ולמנוע התנגשויות. יש לה גם ספריית UI עם אלמנטים מוכנים לשימוש חוזר. Polymer משתמשת בסדרה של polyfills = מימושים בג'אווהסקריפט שמדמים תקן עתידי עוד לפני שהדפדפנים תומכים בו. ברגע שדפדפנים יחלו לתמוך בתקנים אלו (אם וכאשר) – סביר שהביצועים יהיו טובים יותר ושבעיות מסוימות תפתרנה.Polymer כנראה תשתלב יפה עם Angular.js – ספרייה אחרת מבית גוגל (בהמשך הפוסט).
כתבה רלוונטית: Google Believes Web Components are the future of Web Development.

Enyo פותחה במקור עבור מערכת ההפעלה WebOS – אבל נתרמה מאז לקהילה. עקרונות רבים דומים בה ל YUI או Dojo – כך שלא קל לומר מייד במה היא מחדשת. Enyo החלה במקור כספרייה למובייל (כ"ספריית הבית" של WebOS) אבל היא מתאימה לחלוטין גם ל Desktop. נקודת חוזק בה היא היכולת לפתח אפליקציה שתרוץ גם על desktop, גם על mobile וגם על יצירי כלאיים (Phablet, מחשב נייד עם touch וכו'). ל Enyo יש גם חבילת התאמה ל PhoneGap/Cordova לפיתוח Hybrid Web Apps (פוסט רלוונטי).

עדכון: עוד Framework נפוץ למדי הוא ExtJSבשנת 2010 ExtJs חברו לספריות jQtouch (בהמשך) ו Rafael (בפוסט ההמשך) לחברה שנראה Sencha.

ExtJS היא ספרייה עשירה ומלאה בנוסח YUI ו Dojo, אם כי ברמת מודל ההפשטה שלה היא התרחקה צעד נוסף מה DOM/HTML/CSS והיא קרובה יותר למודלים של פיתוח אפליקציות Desktop כגון Spring או AWT. המומחיות של ספרייה זו היא אפליקציות עסקיות ויש לה פקדים עשירים מאוד ליצירת טבלאות וגרפים. ל ExtJS יש כנראה את סט הפקדים העשיר ביותר מבין ספריות הג'אווהסקריפט. התוצרים של ExtJS "כבדים" יותר מ YUI ו Dojo מבחינת כמות ומורכבות ה javaScript שנוצר – ולכן מתאימים יותר לאפליקציות שרצות ב LAN (בניגוד ל YUI ו Dojo שכוונו לאפליקציות אינטרנט). בגרסה האחרונה שלה (4.0) ExtJS עברה הרחבות משמעותיות הוסיפה גם תשתית MVC.

קישור: השוואה בין יכולות הספריות שהזכרנו (מלבד Polymer שעדיין לא נכנסה לטבלה)

ספריות למובייל

מובייל הוא היום הנושא החם, והוא רק הולך וצומח במהירות. סביר שהשפעתן וחשיבותן של ספריות אלו רק ילך ויגבר עם הזמן:
jQuery Mobile (בקיצור JQM) היא לא הספרייה הכי ותיקה – אבל היא מתהדרת במותג "jQuery" שכנראה מאוד עוזר. היא גם דיי טובה. היא ו Sencha Touch מתמודדות זה זמן רב על הבכורה.JQM מתבססת על jQuery והיא פחות או יותר המקבילה למובייל של jQuery UI . היא מספקת מנגנון לחלוקת אפליקציית המובייל לדפים ואת ניהול המעברים בין הדפים – ועושה זו בצורה דיי יעילה.
בנוסף היא עוזרת לטפל ב touch (תכונה חשובה מאוד במובייל) ויש לה סט גדול של פקדים המותאמים למובייל. היא כוללת Theme Roller שמסייע לשנות את צבעי האפליקציה בקלות (אם כי הוא קצת מוגבל) ואפשר למצוא לה עורכי WYSIWYG – לתחילת עבודה מהירה.

הבדל גדול בין JQM ל Sencha הוא ש JQM היא חינמית בעוד Sencha תגבה כסף מארגונים (יש כמה הקלות עבור סטארטאפים ופרויקטי קוד פתוח). Sencha Touch, כמו YUI או Dojo מספקת תמונת עולם (הפשטה) שאיננה מבוססת על HTML, DOM ו CSS – יש שאוהבים זאת ויש כאלו שלא (אני מאלו שלא).

בניגוד ל JQM ו SenchaTouch בחרו בפקדים וחווית UI "ניטרלית" למערכת ההפעלה, KendoUI (יש לבטא: "Can Do UI") מייצר UI שנראה כמו מערכת ההפעלה שהיא רצה עליו: iOS, Android וכו'. אפשר לטעות במבט ראשון באפליקציה ולחשוב שהיא Native Application של מערכת ההפעלה, אולם "גליצי'ם" קטנים פה ושם – יסגירו אותה לאורך הזמן.
אני רואה אותה מוזכרת הרבה במאמרים ובפוסטים – אם כי אני לא מכיר אף אחד שבאמת עובד איתה.
jQTouch (שהשתנה לאחרונה את שמה ל jQT – נראה שג'סטר פספס את זה איכשהו) היא ספרייה דיי ותיקה (מאז 2009?) שלא כ"כ המריאה עד אשר התאחדה עם ExtJs לחברת Sencha. היא:

  • חינמית (כרגע)
  • תלויה ב jQuery – וחולקת איתה את פילוסופיית השמירה על ה abstraction של הדפדפן.
  • מיועדת כרגע רק ל webKit (צעד טקטי, לפי דעתי), בעוד JQM מיועדת לכמעט כל מכשיר מובייל ששמעתם עליו.
  • כמו KendoUI – מספקת UI שמחקה את מערכת ההפעלה עליה היא רצה.

אני מבין את Sencha שרוצה Plan B במידה ויתגלה שהאסטרטגיה של Sencha Touch נכשלת. האם הייתי מהמר באפליקציות שלי על ספרייה שכזו?…

ספריות Client Side MVC

ספריות אלו הפכו כמעט לחלק בלתי-נפרד מפיתוח של כל מערכת בגודל בינוני ומעלה בג'אווהסקריפט (צד-לקוח). במיוחד ב Single Page Applications (בקיצור: SPA).

כתבתי על הקונספט וקצת על הספריות בפוסט הצצה מהירה על Backbone.js

עלי לציין שבעת כתיבת הפוסט ההוא הייתי תחת הרושם ש Backbone, Ember ו Knockout הן שלושת הגדולות ויש עוד "כמה בינוניות" בניהן Angular.js. מאז, למדתי מכמה מקורות ש Angular היא דווקא מאוד פופולרית, כנראה שנייה לאחר Backbone ובעלת אופק חיובי. התחלתי להכיר אותה ולהבין גם מדוע.

Backbone היא עדיין הספריה הפופולרית ביותר – אם כי במגמת ירידה קלה. מתכנתים רוצים יותר ממה שהיא מספקת. אופציה מעניינת אחת הוא plugin פופולרי ל Backbone בשם MarionetteJS שחוסך הרבה "הקלדות חוזרות" שקיימות ה Backbone.
עדיין נראה לי שתוך שנה-שנתיים האחרות יתפסו את הבכורה ו Backbone תהפוך קצת יותר לספריית נישה לאלו שאוהבים הרבה שליטה על הקוד שלהם.

עדיין – זו הספרייה הטובה ביותר להתחיל ללמוד ממנה מהו Client-Side MVC. הייתי עדיין פותח בה בכדי ללמוד את העולם הזה, לפני שהייתי מבצע בחירה אחרת ומשקיע את הזמן הרב הנדרש בלימוד Ember, Angular או Knockout.

ספריות צד-שרת

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

  • גישה לקבצים
  • פעולות Networking
  • ריבוי threads (לפחות נרצה אחד לכל Core של מעבד)
  • הפשטה וכלים לביצוע משימות נפוצות של פיתוח צד-שרת, כגון Express שהיא המקבילה (ה low level) של Servlets על Node.

האם היא תחליף את ג'אווה כפלטפורמה – עדיין מוקדם לומר.

Meteor היא מעין יציר כלאיים: מטרתה לאפשר פיתוח מהיר מאוד של אפליקציות. "לאיפה הולך זמן רב?" החבר'ה של Meteor שאלו – "אהה, כן. לאינטגרציה בין שרת ולקוח". Meteor רצה מצד אחד על Node לכתיבת השרת ומצד שני בצד הלקוח על הדפדפן ועושה את האינטגרציה – קלה מתמיד. היא דיי חדשה – אך יש מסביבה לא מעט באזז.

כותבי אפליקציות עסקיות: זוהי ספרייה שמכוונת להיות Enterprise class ולא עוד "צעצוע".

בחלק הבא

בפוסט ההמשך נראה עוד ספריות חשובות: בדיקות, Template Engines, ויזואליזציה, ניהול תלויות ועוד – וכך נשלים את המספר ל 47. זאת התכנית, לפחות.

שיהיה בהצלחה!

שאלות על Object Oriented Desgin

לפני כשבוע נתקלתי בוויכוח הבא:

במערכת כלשהי, באזור לו נקרא "Sub-Project 3", מחלקה A (בג'אווה) קראה למתודה במחלקה B, אשר דרשה כפרמטר איזה ערך. הערך יכול להיות אחד מ 3 ערכים קבועים – ועל כן המפתחים יצרו enum (נקרא לו ENUM_X). עד כאן – יפה וטוב.

מפתח אחר גילה שבדיוק אותו enum (נקרא לו 'ENUM_X) מוגדר במקום אחר בפרויקט, ודרש שמחלקות A ו B ישתמשו ב enum המקורי – כך שלא יתוחזק "קוד כפול".

המפתח אשר כתב את הקוד במקור טען: "חבל לייצר reference לעוד תת פרויקט ב Build בשביל כזה דבר קטן. שיהיו שני enums זהים – לא יקרה שום דבר."- "אבל אם תשנה אחד ותשכח את השני?! – מה אז?"

הוויכוח התלהט והגיע לראש הקבוצה (!).

מה דעתכם? במי אתם הייתם מצדדים?

כיצד לסיים את הוויכוח?

לפני שאספר לכם מה הייתה הצעתי (שנדחתה פה-אחד ע"י 2 הצדדים, כדרך אגב) ארחיב את הדילמה:

מי שקצת בקיא ב"תיאוריה" של הנדסת תוכנה או Object Oriented Design (בקיצור OOD) – יכול לטעון:
"שכפול קוד הוא אם כל רוע". "יש עיקרון חשוב שאומר שאין לשכפל קוד: כל שינוי קונספטואלי צריך להתרגם בדיוק לנקודה אחת בקוד בה עושים שינוי". "עקרון זה נקרא Don't Repeat Yourself Principle (בקיצור DRY) – וזהו עיקרון ידוע."
קל להתחבר לטיעון הזה: אותו מכירים אותו, כנראה, מקורס התכנות הראשון שלנו.

האם זהו הטיעון המנצח שיפתור את הדיון?

הממ… לא בטוח.
הנה טיעון מלומד אחר:

"אסור למודול להיות תלוי בחלקי-ממשק שאין לו בהם שימוש". במקרה שלנו יצרנו תלות לא רצויה בכל "Sub-Project 7" – כלומר בהרבה מחלקות וממשקים שאין לנו בהם שימוש. הממ… נשמע חמור!
עיקרון זה נקרא The Interface Segregation Principle.

האם ייתכן שעקרונות ה OOD סותרים זה את זה?

כמה שאלות

  • האם יכול אדם, המכיר את 2 העקרונות והוא בעל כושר שכנוע, להחליט באופן רגשי במי הוא מצדד וכל פעם לשלוף את "הטיעון התאורטי המתאים" בכדי להנחית "טיעון מנצח"? האם הוא יכול לעשות זאת מבלי להיות מודע לכך ולהאמין שהוא "רק פועל ע"פ התאוריה"?
  • בהינתן שחוקי ה OOD סותרים לעתים אחד-את-משנהו, האם ישנם חוקים "חזקים יותר" שיש להעדיף?
  • נניח ונוותר על אחד החוקים או שניהם – איזה "נזק" יתרחש? מה ההשלכות של "לא לציית לחוקים"? האם המאמץ הנוסף שבציות לחוקי ה OOD – משתלם?
  • האם OOD היא מתודולוגיה מוצלחת? האם, לאחר כל השינויים בשיטות העבודה שחלו בעשור האחרון – היא עדיין יעילה או רלוונטית?
עסקתי הרבה בחיי ב Object Oriented Design: למדתי, למדתי עוד, ניסיתי, יישמתי, שאפתי ליישום "מושלם", הנחיתי אחרים במתודולוגיה וכו'.
עדיין, ברגע זה, כשאני עומד ושואל את עצמי את השאלות הנ"ל – אין לי תשובה ברורה.
במשך שנים, פעלתי ע"פ כללי הנדסת-תוכנה שלמדתי. פעלתי? – נלחמתי בחירוף נפש, אפשר לומר.
ניסיתי להעמיק כמה שיותר ולעשות את המירב.
כיום אני יודע לומר שנצמדתי במידה רבה, לזרם בתוכנה שנקרא "Defensive Programming". זרם ששפת ג'אווה ו JEE היו אולי רגע השיא שלו. הוא מתבטא ברעיונות כגון:
  • "על המתכנת צריך להגן על התוכנה בפני המפתחים – כולל הוא עצמו".
  • עשה כל מה שתוכל כדי להפחית סיכונים לבאגים.
גישה זו יצרה הרבה משמעת (discipline), אך גם הובילה להמלצות כגון כתיבת מחלקה singleton בג'אווה בתוך enum על מנת להבטיח singleton "שפשוט אי אפשר לקלקל" [א].
מאז, נחשפתי לזרמים אחרים, אולי כמעט הפוכים – שגם הם יכולים לעבוד יפה. הבנתי (פעם נוספת) שאין אמת אחת.

עקרונות ה OOD – למבחן!

עתה אני ניצב מול עקרונות הOOD המוכרים, ואני רוצה להעמידם במבחן הזמן והרלוונטיות.
בניתי רשימה של העקרונות שאני זוכר / מודע אליהם וקיבצתי אותם, באופן גס, ל 3 קבוצות:

חלוקה הקוד למחלקות או מודולים

  • (The Single Responsibility Principle (SRP
  • (Don't Repeat Yourself Principle (DRY
  • Encapsulation
  • High-Cohesion / Low-coupling Principle
  • The Common Closure / Reuse Principle

הגדרת הפשטות (abstactions) / אינטראקציה בין מחלקות

  • The Open-Closed Principle
  • The Liskov Substitution Principle
  • The Release-Reuse Equivalency Principle
  • The Stable Abstraction Principle
ניהול תלויות (מחלקות עד מודולים במערכות)
  • The Interface Segregation Principle + גרסת הקוד שלו
  • (Single Layer Of Abstraction Principle (SLAP
  • The Dependency Inversion Principle
  • The Acyclic Dependencies Principle
  • The Stable Dependencies Principle
עדכון: עקרונות אחרים של תכנון מערכת:

עדכון 2: הנה איזו רשימה דומה של בחור אחר.

באופן גס ניתן לומר שחלוקת הקוד למודולים, הגדרת הפשטות וניהול תלויות היא רוב העבודה ב"הגדרת ארכיטקטורת תוכנה".

נראה לי שאבחר כמה מהעקרונות הנ"ל – ואתחיל לנתח אותם יותר לעומק.
הערות ומחשבות יתקבלו בשמחה.

ליאור

נ.ב: האא כן – איזה פתרון אני הצעתי לדילמה שהצגתי בתחילת הפוסט? אני הצעתי פשוט להשתמש במחרוזת (string) במקום enum וכך להימנע בכלל מתלויות. מה הייתרון של ENUM על מחרוזת פשוטה? Type Safety? ובכן… ה speller ב IDE ימצא את רוב שגיאות הכתיב. בדיקות-היחידה אמורות לתפוס את השאר…

—-

[א] בספר Effective Java 2nd Edition, פריט מס' 3, ע"מ 18. אפשר להתייחס לכל סדרת ספרי ה … Effective – כספרים של זרם ה "Defensive Programming".

כנס JS-il 2013

היי,

עוד יומיים אני מציג בכנס JS-IL 2013:

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

חוץ מהמצגת שלי יש עוד נושאים רבים מעניינים (AngularJS, jQuery Mobile, AMD/require ועוד ועוד).

לינק לכנס (נראה לי שעדיין אפשר להירשם): http://www.js-il.com/

הנה קישור למצגת שאציג:
https://docs.google.com/file/d/0B2nWc6QH9jI3Q3RhM2dLbUZRcFU/view

ליאור

אבני הבניין של האינטרנט: סביבת-הריצה של ג'אווהסקריפט

שפת ג׳אווהסקריפט תוכננה כשפה פשוטה למדי, שפה מפורשת המורצת ע״י מפרשן (interpreter) [מה ההבדל בין מפרשן למהדר]. לא עוד.
המנועים המודרניים של ג׳אווהסקריפט מבצעים קומפילציית JIT לקוד מכונה יעיל למדי – ועל כן הם מתהדרים בשם "מנועים". הביצועים, השתפרו בסדר גודל בכמה השנים האחרונות. הגרסה הבאה של ג׳אווהסקריפט (שם קוד Harmony) כוללת אלמנטים רבים חדשים בשפה שיעזרו לתמוך במערכות קוד גדולות (מחלקות, מודולים, ועוד).

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

מפת התמצאות

הנה מנועי הג'אווהסקריפט של כמה מהדפדפנים הנפוצים:

עץ המשפחה של מנועי הג'אווהסקריפט. מקור.
JIT Compilation (קימפול קוד לג'אווהסקריפט לקוד מכונה לפני ההרצה) היא כבר תכונה סטנדרטית של מנועי הג'אווהסקריפט. Crankshaft (בעברית: גל ארכובה) ו TraceMonkey עד OdinMonkey הם בעצם JIT compilers (בהפשטה) של מנועי הג'אווהסקריפט V8 ו SpiderMonkey בהתאמה. הקומפיילר הוא רק חלק מסוים מהעבודה שמנוע הג'אווהסקריפט מבצע.
כל המנועים המודרניים (כרום +13, פיירפוקס +4, ספארי +5.1, אינטרנט אקספלורר +9) תומכים* ב ECMAScript 5.1 – שהיא הגרסה האחרונה של שפת ג'אווהסקריפט. בעת פוסט זה התמיכה ב ECMAScript 6 (הגרסה הבאה) היא חלקית למדי.
* חריגות קלות: IE9 שלא תומך ב "use strict" ו Safari שעדיין לא תומך ב prototype.bind

טעינת הסקריפטים וסדר ההרצה

הרצת קוד ג'אווהסקריפט כוללת 3 שלבים מרכזיים: פענוח (parsing), אופטימיזציה ורישום הפונקציות (function resolution) והרצת הקוד (execution).
שלב 1: שלב הפענוח
בשלב זה מפרשים את "עץ התוכנית" של קוד הג'אווהסקריפט, מבצעים תיקונים קלים (הוספת נקודה פסיק בסוף משפט, דבר שעלול לעתים להסתיים בקוד שגוי) והמרה לשפת ביניים יעילה יותר (JIT compilation).
אם המפענח נתקל בשגיאה בקוד (למשל: פונקציה שלא נסגרה) הוא מדלג על קטע הקוד וממשיך לבלוק הבא. מדיניות סלחנית זו נקבעה עוד בימים בהם עוד היה מקובל לכתוב "בלוקים" של ג'אווהסקריפט בתוך ה HTML, כך שדילוג על בלוק יחיד היה יכול לאפשר הרצת דף בצורה נסבלת. תקלה בקובץ ג'אווהסקריפט תגרום לדילוג על כל הקובץ – מה שקרוב לוודאי וישבית את כל התוכנית.
שלב 2: שלב האופטימיזציה ורישום הפונקציות

בשלב זה עוברים על קוד הג'אווהסקריפט (בצורת "bytecode") ומבצעים בו שיפורים:

  • inline של פונקציות קצרות לתוך הקוד שקורא להן
  • hoisting – העברת ההגדרה של משתנים ופונקציות לתחילת ה scope בהן הן הוגדרו. מנגנון זה, כך שמעתי, החל כאופטימיזציה ללולאות for אך הפך לחלק מהשפה, וחלק שגם יכול לגרום לבעיות (הסבר מיד).
  • inline caching – הוספת שכבה של caching מעל אובייקטי ג'אווהסקריפט, אחת מהאופטימיזציות היעילות בשנים האחרונות. מקור1 מקור2.
  • inline של קבועים (משתנים שבכל התוכנית אין להם השמה).
  • ביטול קוד שאי אפשר להגיע אליו ("dead-code elimination").

הנה דוגמה המסבירה את מנגנון ה hoisting. אפשר למצוא עליו הסבר נוסף בפוסט מבוא מואץ לג'אווהסקריפט (חפשו: hoisting).

קוד שנראה כך:
בעצם מתורגם, לאחר ה hoisting, לכך:
שלב 3: שלב ההרצה
הרצת הקוד, כפי שהיינו מצפים שיקרה.

ג'אווהסקריפט והדפדפן

לכל טאב או חלון בדפדפן מוגדרת סביבת ג'אווהסקריפט עצמאית משלה. בסביבה זו יש אובייקט בשם window המהווה את ה context הגלובלי. אם אגדיר var x גלובלי בדף אחד הוא יהיה מופע שונה מ var x גלובלי בדף אחר (בהגדרת "var x", בעצם הגדרתי member חדש בשם x על האובייקט window). באופן דומה, פקודות שניתן לייחס בטעות לשפת JavaScript כגון setTimeout או alert הן בעצם פקודות על האובייקט window (הערך ב mdn), כלומר window.setTimeout או window.alert.

את האובייקטים שמספקת סביבת הדפדפן נוטים לחלק לשני אזורים:

BOM – אובייקטים הקשורים ל Shell של הדפדפן.
DOM – אובייקטים הקשורים לדף ה HTML המרונדר.

ה DOM הוא מורכב למדי ומעבר ל scope של פוסט זה (הנה לינק לספר טוב אונליין).
להלן סקירה מהירה של האלמנטים המרכזיים ב BOM:

  • window – חלון[א] או טאב של הדפדפן. מלבד תפקידו כ context הגלובלי ואובייקט האב, יש על אובייקט ה window מתודות למדידת גודל חלון הדפדפן, מצב ה Scroll ומספר אירועים שקשורים לשינוי גודל החלון או ה scrolling.
  • location – מתאר את ה URL הנוכחי של החלון. ניתן לקרוא אלמנטים, לשנות או להאזין לשינויים. שם מדויק יותר היה פשוט "url".
  • history – היסטריית שינויי ה location בדפדפן. ניתן להפעיל פקודות back, forward לדחוף או להסיר אלמנטים מההיסטוריה.
  • navigator – פרטים על הדפדפן: user agent string, תמיכה בג'אווה או ב cookies, סוג וגרסת הדפדפן וכו'. קרוב לוודאי שמקור השם הוא בדפדפן Netscape) Navigator) ומאז הוא איתנו. שם מדויק יותר היה פשוט "browserInfo".
  • screen – פרטים על התצוגה המסופקת לדפדפן: רזולוציה, עומק צבע וכו'.
  • frames – רשימת ה frames או ה iframes שבתוך החלון.

תגית , משלבת בתוך המסמך מעין "מופע חדש של דפדפן" עם אובייקט windows משלו, document משלו, היסטוריה משלו ועוד (הערך ב mdn). השימוש העיקרי של ה iframe הוא להציג בצורה מבודדת, בתוך חלון, תוכן שתוכנן לרוץ בדף עצמאי (לא מתנהג בצורה "חברותית") או שמגיע מ domain אחר. מבחינות מסוימות iframe היא גישה מיושנת ומפתחי ווב רבים רואים אותה כ deprecated, מצד שני ליכולותיה עדיין אין תחליף – ולכן עדיין משתמשים בה.

הערה: השם iframe הוא קיצור של internal frame. ישנה גם תגית בשם frmae שהיא כמעט זהה, אך מחויבת להיות בתוך תגית frameset. כל מה שאומר על iframe נכון בעיקרון גם ל frame.

כל iframe הוא בעצם context חדש בדפדפן, כלומר אובייקט window, מרחב גלובלי חדש, DOMTree חדש וכו'. הוא כמעט כמו "חלון חדש בתוך חלון קיים" בדפדפן, מלבד 2 הבדלים:

  • ניתן לגשת מ iframe אל משתנים / פונקציות ב top frame (כלומר ה context המקורי של הדף). כל זאת תחת מגבלות ה Single Origin Policy – נושא אליו נצלול בפוסט אחר בסדרה.
  • עבור חלון חדש יהיה thread חדש בדפדפן, בעוד iframes באותו חלון משתפים את אותו ה thread. הצורך בשיתוף thread נובע מהדרך בה מתרחשת מקביליות בדפדפן – נושא שנדון בו מיד.

מודל המקביליות בדפדפן: Thread יחיד

לכל חלון פיסי בדפדפן יש thread יחיד. גישה זו היא שונה למדי ממודל ה threads של Java או NET.
היתרונות של גישה זו הם:

  1. יעילות גבוהה מאוד בקוד עם הרבה פעולות IO. כדאי להזכיר: Threads הוא מודל שעיקרו להקל על המפתח, ואינו הדרך היעילה להגיע לביצועים גבוהים [זהירות, פוסט].
  2. פשטות הנובעת מכך שאין צורך לדאוג לסנכרון. Deadlock או Racing Condition הם פשוט לא אפשריים כאשר ישנו thread בודד.
  3. מודל שהתאים מאוד ל"דפי אינטרנט דינמיים" – כלומר אתרים עם אינטראקציה ולא בהכרח אפליקציות.

לגישה זו יש גם כמה חסרונות משמעותיים:

  1. העובדה שב javaScript כל פעולות ה IO הן אסינכרוניות בהכרח [ב] יכולה להקשות מאוד על המעקב אחר flow בקוד. Promises [זהירות, פוסט] היא אחת הדרכים הנפוצות להתמודד עם קושי זה.
  2. אי-יכולת לנצל יותר מ core אחד במעבד.
  3. קוד שכולו CPU (כגון חישוב מתמטי מורכב) יתקע ממשק המשתמש ויהפוך את האפליקציה ללא רספונסיבית.

החסרונות הם אכן משמעותיים ובהמשך נראה את דרכי התמודדות איתם. סה"כ גישת "ה thread הבודד" זלגה כיום לצד-השרת (node, nginx וכד') משיקולי יעילות.

ה Event Loop

אני מניח שאתם מגיעים עם רקע במדעי המחשב (Java, ++C או NET.) והרעיונות של מחסנית (stack) וערימה (heap) אינם זרים לכם.

בג'אווהסקריפט המודל דומה למודל הערימה והמחסניות של השפות הנ"ל, אך יש בו כמה שינויים:

  • ישנו stack יחיד – מכיוון שיש thread יחיד.
  • ישנו Queue – עליו נדבר מייד.
  • האובייקטים ב Heap הם בעיקר Closures (אם כי לא רק) – כך שהתלות בחלק הפומבי שלהם היא מה שמונע מהחלק הפנימי להתנקות ע"י ה garbage collector.

ה thread רץ במין לולאה אינסופית שנראית בערך כך:

מה ממלא את ה Queue?
  • פעולות IO שנסתיימו, כגון ajax.$ (שזו בעצם עטיפה יפה לאובייקט ה XmlHttpRequest).
  • events של הדפדפן כגון DOMElement.onClick או window.resize.
  • פעולות setInterval ו setTimeout. למי שלא מכיר, פעולות אלו מבקשות להפעיל פונקציה נתונה בעוד זמן מסויים (setTimeout) או כל פרק זמן קבוע (setInterval).

כל הפעולות הנ"ל לא יופעלו לעולם ע"י thread אחר (כאשר מדובר באותו החלון). הן יופעלו כאשר ה thread הראשי ביצע את כל הקוד שלפניו, רוקן את המחסנית מקוד להרצה – ורק אז הוא זמין לטיפול ב"בקשות אחרות". אם נעמיס על ה thread הראשי פעולות חישוב כבדות, כל האירועים שממתינים ב Queue "ייתקעו" (ואיתם תתקע האפליקציה) עד אשר ה thread הראשי יתפנה אליהם.

הנה דוגמת קוד שתעזור להדגים את התנהגות ה event loop:

והנה תוצאת ההרצה:

חדי העין ישימו לב ש "work for later" יצא 3 ולא מה שהיינו מצפים: 0 עד 2. הסיבה לכך היא שהערך של i מוערך בעת הרצת הקוד (הקוד נקשר ל global closure) ולא בעת הקריאה ל setTimeout, כפי שאולי יכול להשתמע.

הפתרון הוא כמובן להעביר את i כפרמטר לפונקציה בעת הקריאה ל setTimeout:

בפעולת setTimout, פרמטרים נוספים לאחר הזמן להמתנה (במקרה שלנו: 0) פשוט יועברו כפרמטרים לפונקציה הנרשמת. דפדפני אינטרנט אקספלורר הקודמים לגרסה 10 לא תומכים בהתנהגות זו.

תורת היחסות של סביבת-הריצה של ג'אווהסקריפט

שאלת טיזר: כמה intervals של 100ms קיימים בשנייה אחת?
תשובה מתמטית: עשר.
תשובה בסביבה-הריצה של ג'אווהסקריפט: ניתן לקוות ל 9, אבל לפעמים גם רק 2.

הא?! נסביר מייד.

מה ההבדל בין 2 צורות הכתיבה הבאות?

דעה רווחת היא שההבדל הוא שאופציה א' תפעל כל INTERVAL קבוע, בעוד אופציה ב' תפעל כל INTERVAL + הזמן שלוקח לבצע את doSomeWork.

תיאור זה הוא קרוב למציאות – אך איננו נכון. נאמר שקבענו setTimeout להחליף אייקון על המסך בעוד 100ms. ברגע שנזרק האירוע (timer) – מי ייקח את הקוד ויבצע אותו? אם ה main thread שקוע בתוך קוד שבמחסנית – אין מי שיטפל בבקשה. כל מה שיקרה בעקבות ה timer הוא רישום הפונקציה שביקשנו להריץ ל Queue. היא תטופל רק ברגע שה main thread יתפנה (וכל האלמנטים הקודמים ב Queue יטופלו).

ההבדל המדויק, אם כן, בין setInterval ("אופציה א'") ל setTimeout ("אופציה ב'") היא שבאופציה א' יבצוע רישום של הפעלת הפונקציה doSomeWork ל Queue כל INTERVAL. בכדי להגן על ה Queue מפני זליגה (Queue Overflow), הפעלת הפונקציה תרשם רק אם היא איננה רשומה כבר ב Queue – ז"א לא יהיה יותר מעותק אחד שלה ב Queue.
באופציה ב' יהיה רישום ל Queue ברגע שאנחנו קראנו לפקודת setTimeout – וכאן אין מנגנון הגנה מפני רישום חוזר (כי פחות סביר שהוא יקרה ללא שהתכוונו לכך). מצד שני ההפעלות לא ינסו להתרחש בפרקי זמן קבועים.

שימו לב שלמנגנון ה timers של הדפדפן ישנם חוסרי-דיוק משלו הנובעים מחלוקת העבודה במערכת ההפעלה וה threads בתוך הדפדפן. כדאי להניח על שגיאות של פלוס/מינוס 5ms בהפעלת ה timers.

חזרה לטיזר בתחילת הפסקה: כיצד אם כן ייתכן ש setInterval של 100ms יתרחש רק פעמיים בשנייה?

הנה דוגמת קוד שגורמת לכך:

כמה הערות על הקוד:

  • ()performance.now היא דרך מדויקת ואמינה יותר מ ()Date.now – עבור מדידת ביצועים.
  • באופציה ב' תנאי היציאה מתרחש לאחר 9 פרקי זמן, מכיוון שפעולת הרישום מתרחשת כ INTERVAL אחד לפני שהקוד ירוץ בפועל.

העבודה שנעשית ב doSomeWork נראית אמנם דיי אגרסיבית, אבל יש גם תנאים מקלים:

  • הרצתי את הבדיקות על about:blank – כלומר דף ללא StyleTree ו DOM מינימלי.
  • הרצתי את הבדיקות על דפדפן כרום גרסה 27, ועל מעבד i5 שולחני שרץ במהירות 4Ghz – הקצה הגבוה של החומרה הסבירה שעליה תרוץ אפליציית ווב בימנו.

סה"כ סביר אפליקצית ווב מורכבת תגיע לרגעים שהיא תבצע עבודה רבה באותה המידה (במיוחד לפני ביצוע אופטימיזציות בקוד!)

הנה התוצאות:

לא ניקיתי את ה DOM בין ההרצות כך שלהרצה מאוחרת יותר – יש עבודה קשה יותר בכל הרצה של ()doSomeWork.

הרצה ראשונה
כבר כאן אנו רואים שמנגנון ה setInterval (יוצר ה"טיק") מזייף ואפילו מדלג על 3 טיקים (5, 7 ו 9).

הרצה שלישית
הנה 2 טיקים בלבד בשנייה. מי אמר שאין עוד אירועים שמתרחשים? (כמו "טוק")

קצת יותר ברצינות: זמן של 100ms הוא "זמן דפדפן" ארוך, ואם הקוד שלכם יעיל – אינכם אמורים להגיע ל2 טיקים של 100ms בשנייה. מצד שני, ישנם רגעים שבהם הדפדפן עובד קשה יותר – ועליכם להניח שטיקים מסוימים "יאבדו", או לפחות יתבצעו באיחור.

2 לקחים מעשיים

טריק ידוע בכדי "לא לתקוע" את הדפדפן עם פעולות חישוב יקרות הוא לעצור אותן בנקודות ציון מסוימות ו"לדחוף" את המשך הפעולה ל Queue בעזרת (setTimeout(calcFunction, 0. פעולת ההמשך ("calcFunction") תרשם לסוף ה Queue ותאפשר לאירועים אחרים להתרחש. ההבדל יכול להיות דרמטי בין "דפדפן תקוע" (שהמשתמש מנסה לסגור) ל "דפדפן מגיב".

דרך (עקיפה) אחרת לשחרר עבודה מה Thread הראשי (כלומר: היחידי) היא לשלב יכולות שעובדות באופן מובנה על threads אחרים בדפדפן. דוגמה טובה לכך תהיה שימוש ב CSS Animations המבוצעים ע"י ה thread של מנוע הרינדור – וכך משחררים את ה Thread הראשי להריץ קוד ג'אווהסקריפט. מעבר לכך – כנראה שניצלתם בדרך זו core אחר של הCPU שעד כה היה בכלל מובטל!

Web Workers

לצורך התמודדות עם חישובים ארוכים, או בכדי לנצל בצורה מירבית מעבד בעל מספר ליבות, נבנה תקן ה Web Workers – תקן חדש יחסית המאפשר להאציל עבודה מוגדרת מה Thread הראשי על Threads אחרים. עבודה עם web workers היא מעט מסורבלת מכיוון שה threads השונים אינם משתפים זיכרון – עליהם לתקשר בעזרת הודעות בלבד (רעיון דומה ל Actors, אותם הזכרתי בעבר).

הצורך בהעברת ההודעות בין ה web workers ל main thread נובע ממודל ״ה Thread היחיד״ של הדפדפן: אם הייתה גישה ישירה לזיכרון משותף – אנו נחשפים לבעיות concurrency אפשריות. באופן דומה אילוץ זה הכתיב שאם frames באותו חלון יכולים לתקשר אחד עם השני – אסור להם לרוץ ב threads נפרדים והם חייבים לשתף thread.

ההודעות שעוברות בין web workers עוברות by-copy ולא by-reference מהסיבות הנ"ל. נראה שרוב הדפדפנים ממש מעבירים אותן כהודעות IPC שעוברות serialization / de-serialization בכל פעם.

הנה רשימה קצרה של מה ש Web worker יכול / לא יכול לעשות.

ל Web Worker מותר:

  • לשלוח הודעות ל Workers אחרים.
  • ליצור ולהרוג workers אחרים.
  • לגשת (קריאה-בלבד) לאובייקטי ה navigatior וה location של ה BOM.
  • לבצע קריאות ajax (ע"י שימוש ב XmlHttpRequest).
  • להשתמש ב timers של הדפדפן.
  • לטעון קבצי ג'אווהסקריפט נוספים.
  • לפתוח connections לשרת, בעזרת web sockets.
  • לגשת ל cache ו local storage.

ל Web Worker אסור:

  • לגשת ל DOM
  • לגשת לאובייקט ה window (מלבד 2 החריגות לעיל).

סיכום

זה היה פוסט ארוך לכתיבה!
כתיבת פוסט זה הזכירה לי את קורס "מערכות ההפעלה" באוניברסיטה, וכנראה לא סתם. מערכת ההפעלה הייתה סביבת הריצה (Runtime Environment) של שרתים ואפליקציות שולחניות (לפני ש virtual machines נכנסו באמצע ושינו חלקית את התמונה).
כיום, הדפדפן הופך להיות סביבת ריצה חשובה לחלק הולך וגדל של אפליקציות, ועל כן הכרת סביבת ריצה זו – חשובה לא פחות.

שיהיה בהצלחה!

—–

[א] חלון חדש של דפדפן יכול להיות עדיין שייך לאותו process של מערכת ההפעלה.

[ב] מקרה קצת יוצא דופן הוא modal dialogs בדפדפן כגון window.confirm או window.alert. זו תכונה של הדפדפן ולא של שפת ג'אווהסקריפט.

מקורות נוספים:

הרצאה ב Velocity 2011 על מנועי ג'אווהסקריפט
כמה טיפים מעניינים לקראת סוף המצגת.
http://velocityconf.com/velocity2011/public/schedule/detail/18087

הרצאה על פרטי המימוש של מנועי ג'אווהסקריפט, מכנס SenchaCon 2010
כניסה לפרטים טכניים יותר ממה שכיסיתי בפוסט זה.
http://vimeo.com/18783283

רינדור בצד הדפדפן

פוסט זה משלים בעצם 2 נושאים שונים שכתבתי עליהם לאחרונה:
  • הפוסט משלים את נושא מנוע הרינדור מהפוסט הקודם (א.ה.ש.ה: "הדפדפן").
  • הפוסט משלים את הפוסטים על "ביצועים של אפליקציות אינטרנט" ונותן טיפים חשובים לשיפור זמני-רינדור בדפדפן.
לאחר חשיבה, הוספתי את 2 הפוסטים על "ביצועים של אפליקציות אינטרנט" לסדרה (במקום לפתוח סדרה חדשה על ביצועים בווב) – זאת מכיוון שלא ניתן לעסוק בביצועים ללא הבנה של ה internals, ומכיוון שהבנה של ה internals מובילה מפתחים לשיפורי ביצועים.פוסט נוסף שכרגע בכתיבה יעסוק בסביבת הריצה של הג'אווהסקריפט בדפדפן, וישלים כמה נקודות אודות ביצועים בצד-הדפדפן שלא נכנסו לפוסט זה.

פוסט זה שייך לסדרה אבני הבניין של האינטרנט.

שינוי פרדיגמה

במשך שנים נהגנו למדוד ביצועי אפליקציות ווב ע"פ:

  • מספר ה round-trips בין הדפדפן לשרת – שאפנו בקנאות למינימום.
  • כמות התעבורה ברשת ב kb – עמלנו על מנת לצמצם.
  • CPU של תהליך הדפדפן – שייחסנו אותו ברובו למשהו שמעבר לשליטתנו.
כל זה טוב ויפה סביר כאשר מדובר בדפי HTML פשוטים יחסית שנבנים בצד השרת, נוסח JSP או ASP.NET.

באפליקציות ווב מודרניות המצב התהפך:

רוב זמן הרינדור הוא באופן ברור בצד הלקוח.
ע"פ Alexa, כ 80% מזמן רינדור הדף ב 1000 האתרים המובילים מושקע בצד הלקוח.

חלק נכבד מהשיפורים האפשריים – הוא בצד הלקוח.
לא מזמן נתקלתי במקרה בו הצליחו להוריד את זמן הרינדור של דף מ 11 ל 2.5 שניות ע"י שינוי בקוד הג'אווהסקריפט בלבד. זהו מקרה מעט קיצוני, אך אמיתי.
באפליקציות ווב, ניתן לשפר רבות את "הזרימה החלקה" של האפליקציה ע"י שינויים קטנים ב CSS ובקוד הג'אווהסקריפט שמשתמש ב DOM API – רק צריך להכיר כמה עקרונות שאסביר בפוסט זה. שינויים אלו משמעותיים במיוחד לשימוש ב touch בהם עיקובים ברינדור ("תקיעה") היא מציקה במיוחד מכיוון שהציפיה היא שכל האפליקציה "תעבוד חלק"…

מקור
יתרה מכך, ברור כיום יותר ויותר שלא "זמן ה CPU של הדפדפן שהושקע" הוא החשוב. גם לא "הזמן עד הסוף המוחלט של רינדור הדף".
בעצם האלמנט החשוב ביותר היא תחושת המהירות של משתמש הקצה, שמושגת ע"י הצגת תוכן חלקי ("משהו") מוקדם ככל האפשר. עקרון זה נקרא Perceived Performance והרשת מלאה בחומר בנושא, כך שאינני מרגיש שיש טעם שאוסיף.
דף שעולה מהר, מול דף שנותן הרגשה שהוא עולה מהר. מקור.

האם כל אופטימיזציות הרשת (כגון דחיסה ואיחוד של משאבים), שעסקנו בהן בפוסטים קודמים, הן מיותריות? כל וכלל לא. דווקא אופטימיזציות הרשת הן לרוב הקלות, אך עבור אפליקציות ווב – הן פשוט לא מספיקות. אציין גם שחלק מאופטימיזציות "הרשת" משפיעות לטובה, באופן עקיף, על ביצועי הדפדפן.

מדוע רינדור הדף "עוצר" בזמן טעינת הדף?

כפי שתיארנו בפוסט קודם, קובץ ה HTML עובר פירסור והוא זה שגורם לטעינת קבצי הג'אווהסקריפט / CSS:

שלב 2: כאשר ה HTML Parser נתקל בתגיות המתארות קבצים אחרים – הוא טוען אותם.

אלמנט בעייתי בשלב טעינת קבצי הג'אווהסקריפט הוא היכולת של הסקריפט לשנות את קובץ ה HTML – במיוחד בעזרת פקודת document.write. בפקודה זו, שנראית תמימה לכאורה, יש מלכוד: היא מוסיפה markup לסוף ה DOM Tree ולכן יש משמעות שונה אם נריץ אותה עכשיו, או בעוד כמה מילי-שניות, לאחר שעוד markup פורסר והוסף לסוף ה DOM Tree.

בכדי להבטיח את נכונות טעינת הדף, ברגע שהדפדפן מזהה בקשה לטעינת סקריפט כלשהו (תגית ) ועד רגע שהסקריפט סיים לרוץ – הוא "מקפיא" את מנוע פירסור ה HTML. הקפאה זו באה ברגע קריטי ביותר: טעינת הדף הראשונית!
התוצאה האפשרית: מסך לבן למשתמש הקצה עד אשר כל הסקריפטים ירוצו (חוויה מוכרת מאתרים רבים) = perceived performance גרוע. מצב זה מחמיר ככל שהאפליקציה גדולה וטוענת כמות גדולה של ג'אווהסקריפט.
אפרופו: Internet Explorer, עד IE8, הגדיל לעשות והפסיק גם את טעינת הרשת של משאבים אחרים. עד היום לא ברור מדוע.
ישנן מספר דרכים להתמודד עם הבעיה הזו:
  1. להעביר את הבקשות לטעינת סקריפטים מה HEAD לתחתית ה HTML, ממש בסוף ה BODY [א].
  2. לציין על הסקריפטים ש "הם ילדים טובים" בעזרת תווית async (מייד)
  3. להשתמש בספריה כגון require.js שעושה את שני הדברים עבורכם [זהירות, פוסט].
תווית async ניתן לשים על סקריפטים ש:
  • לא מבצעים כתיבה ל DOM בעזרת document.write בזמן ההרצה.
  • סקריפטים אחרים לא תלויים בהם – בשלב ההרצה של הסקריפטים האחרים. כלומר הסקריפט האחר יכול לכלול פונקציות שקוראות לסקריפט שלנו, אל לא לפני שהיה ארוע document.ready.

אם הקוד שלכם מובנה ומסודר – קרוב לוודאי שזה כבר המצב.

המשמעות של תווית ה async היא שהדפדפן יטען את הקבצי הג'אווהסקריפט באופן מקבילי לפירסור HTML (ואחד לשני). כלומר – אין הבטחה באיזה סדר הם ירוצו. יש לנקוט בקוד הג'אווהסקריפט מעט מאמצי הגנה כגון הגדרת namespace בכל קובץ או וידוא שקוד ה bootstrap קשור לאירוע של הדפדפן (כגון document.ready) – לרוב זה לא מאמץ גדול.

תווית נוספת היא תווית defer שמאפשרת לסמן סקריפטים שיכולים לרוץ מאוחר יותר (כגון כפתור "feedback"). סקריפטים אלו יטענו במקביל כמו קבצים שסומנו כ async אך ירוצו רק כאשר כל משימות הפירסור הנוכחיות – הסתיימו.

—–
הערת צד: בעיית "עצירת פירסור ה HTML" היא כ"כ חמורה שהדפדפנים מנסים בכל זאת להתמודד איתה, גם בלי עזרת המפתחים:
Firefox: ברגע שהוא מתחיל להיתקל בסקריפטים, הוא שומר עותק של ה DOM Tree בצד (להלן DT'). משם הוא ממשיך לפענח ולבנות את ה DOM Tree (להלן DT) תוך כדי שהוא עוקב אחר התנהגות הסקריפטים.
אם לא הייתה פעולת document.write הוא משתמש ב DT – שהוא כבר הספיק לעבוד עליו בקביל בזמן שהסקריפטים נטענו.
אם הייתה פעולת document.write (המצב הנדיר) אזי הוא חוזר ל DT' וממשיך לבנות את ה DOM Tree מנקודה זו.
Chrome: ברגע שהוא נתקל ב Script ראשון, יפעיל את ה Proload Scanner שימשיך לפענח ולסרוק את ה HTML בכדי לחפש אחר קבצי סקריפט נוספים ולהתחיל לטעון את הקבצים מהרשת במקביל. "רישום הפונקציות" וההרצה של הסקריפטים עדיין תעשה ע"פ הסדר שבו הסקריפטים הופיעו במסמך ה HTML – בכדי להבטיח את תקינות ההרצה.
—–

HTML דינאמי – ההשפעה של פעולות DOM מתוך קוד ג'אווהסקריפט

כפי שהסברנו בפוסט הקודם, כתיבה ל DOM היא פעולה יקרה, מכיוון שמלבד שינויים ב DOM Tree היא דורשת בנייה מחדש של ה Render Tree, חישוב Layout מחודש ורנדור מחדש של חלקים מה Render Tree לגרפיקה על המסך.

המשמעות של כתיבה ל DOM

העלות היא, כמובן, אדירה – במיוחד כאשר מדובר באפליקציה גדולה.

דמיינו אפליקציה עם מאות nodes ב Style Tree. כל הכנסה של אלמנט ל DOM תדרוש מעבר על כל החוקים בתת העץ ב Render Tree. אם מבוצעות הכנסות חוזרות לאותו תת-עץ ב DOM Tree – יהיו בדיקות ונשנות של DOM Tree Nodes כנגד אותם חוקים – עבודה מיותרת בעליל.

מפתחים שמבינים שכתיבה ל DOM היא יקרה, עוברים בד"כ את השלב בו הם מנסים לבנות "תת עץ DOM" בצד ולהכניס אותו ב"מכה" אחת. למשל – כ String. פעולה זו לרוב לא תציג שיפור בתוצאות – מה שעלול להשאיר את המפתחים מבולבלים.

הסיבה שאנו לא רואים שיפור בין 2 הגישות היא מכיוון שכותבי הדפדפנים חשבו עלינו – ועשו את העבודה עבורנו:
כתיבות ל DOM Tree עוברות דרך Write Buffer שיבצע Flush (שפיכת כל השינויים שהצטברו ב buffer) בנקודת הזמן המאוחרת ביותר האפשרית.

מה שמעניין הוא מה היא" נקודת הזמן המאוחרת ביותר האפשרית"? ובכן – זו הנקודה בה קורה אחד מ 2 דברים:

  1. מישהו מבצע פעולת read מתוך ה DOM Tree או ה Render Tree – והדפדפן רוצה שהוא יקבל תשובה עדכנית לאור הכתיבות שנעשו קודם לכן.
  2. תזמון קבוע שנקבע בכדי להציג שינויים למשתמש. רוב הדפדפנים היום קבעו לעצמם קצת רענון של 60fps, כך שכל 16.67 מילי-שניות הדפדפן מעדכן את ה DOM בכדי שיוכל להציג את השינויים שנעשו על ה Canvas, כך שיגיע למשתמש הקצה.
    אופטימיזציות שונות עשויות לזהות שפעולה אינה בעלת משמעות על המסך (למשל שינוי של אלמנט שהוגדר כ display:none) וכך לדלג על פעולת flush שנובעת מתזמון.

שימו לב שקריאות שונות של ה DOM API ניגשות למבני נתונים שונים. לדוגמה:

  • קריאת getAttribute או innerHTML – תגרום לקריאה מה DOM Tree. (מסומנת כ "(Read (A" בתרשים למעלה)
  • קריאת scrollHeight או scrollWidth (שהן פשוט הדרך לקבל את הגובה / רוחב של האלמנט) – תגרום לקריאה מה Render Tree. (מסומנת כ "(Read (B" בתרשים למעלה)

הערת צד: אל תגלו לאף אחד, אבל ה Render Tree הוא בעצם אינו עץ אחד אלא מספר מבני-נתונים בעלי חפיפה-חלקית שמרכיבים את התמונה השלמה. עובדה זו אינה תורמת לדיון – ולכן אשאר עם ההפשטה של ה "Render Tree".

המסקנה מהדרך בה עובד ה DOM Tree Write Buffer היא חשובה:

  • אם נבצע 100 כתיבות ל DOM ולאחר מכן 100 קריאות – תהיה פעולת Flush אחת.
  • אם נבצע לולאה של 100 x (כתיבה + קריאה מה DOM) – יהיו 100 פעולות Flush.
ההבדל (כפי שאפשר לראות בדוגמה בפוסט, לחצו על הכפתורים האדומים) היא בין פעולה ש"תוקעת" את ה UI לחצי-שנייה עד שנייה, לבין התנהגות חלקה למדי. הבדל בין ממשק "זורם" לממשק "תקוע".

דרכי קיצור ברינדור הדף: דילוגים על שלבים לא-נחוצים.

הדפדפנים לא סיימו את האופטימיזציות ביצירת ה DOM Tree Write Buffer. יש להם סדרה של דרכי קיצור לרנדר את הדף בצורה יעילה יותר, תוך כדי דילוג על פעולות מיותרות.פעולות של דילוג על בניית ה Render Tree וה Re-layout

הדוגמה הקלה לתיאור היא פעולות ציור. כאשר אנו משנים צבע או רקע (צבע או תמונה) של אלמנט, אין צורך לחשב מחדש את הגובה / רוחב / מיקום של כל האלמנטים על הדף (==> Re-layout). אין גם צורך לבנות מחדש תתי-עצים ב Render Tree, כי אין סיכוי שהם יישתנו. אפשר לשנות רק את הצבע/רקע על האלמנט הספציפי ו"לקפוץ" ישר לרינדור ה Canvas.

הדפדפן מצדו ירנדר רק את ה"מלבן" בו היה השינוי, כך שברוב הפעמים זו פעולה פשוטה למדי. ניתן לראות אילו חלקים ב Canvas מרונדרים מחדש בעזרת כלי הפיתוח של כרום:

באופציות של כלי הפיתוח (חתכתי את התמונה רק לחלק הרלוונטי) יש לסמן את "Show Paint Rectangles" ואז כל אזור ב Canvas שצויר מחדש יופיע לשנייה מסומן במלבן אדום.
אם אתם רואים ששינוי קטן מבחינתכם גרר ציור של אזור גדול ב Canvas – כדאי לחקור את הסיבה.

פעולות המדלגות על כל שלבי הרינדור

הוספת class או attribute על אלמנט, כאשר class זה לא מחיל על האלמנט שום חוק חדש. עדיין יחושבו כל חוקי ה CSS מול האלמנט ותת העץ שלו, אך לא יבנו מחדש האלמנטים ב Render Tree, לא יהיה Layout ולא יהיה רינדור מחדש ל Canvas.

פעולות היכולות לדלג על ה Re-Layout

למשל:

  • שינוי visibility (למשל none ל visible) – מכיוון שאלמנטים אלו עדיין נשמרים ב Render Tree.
  • שינוי מיקום לאלמנט "מרחף" שאינו חלק מה Layout (או לפחות ה Layout שיושפע ממנו יהיה קטן בהרבה).
  • פעולות transform

אולי כדאי לסכם פעולות אותן כדאי לנסות ולצמצם / לרכז כאשר עובדים על שיפור ביצועי ה rendering:

פעולות שיגרמו לשינויים ב Layout:

  • שינוי גובה, רוחב
  • הוספת אלמנטים / הסרת אלמנטים נראים.
  • שינויים בטקסט (כולל, למשל, line-height או word-spacing)
  • שינויים ב scroll
  • שינויים ב border-width (אך לא בצבע או border-radius, אלו פעולות שישפיעו רק על ה render / paint)
  • שינוי פונטים
  • שינויים ב margin או padding
  • שינוי Display מערך inline ל block (או להיפך)

פעולות שיגרמו לבניית תתי-עצים ב Render Tree

  • Display (מעבר מ/אל "none")
  • Transform (מעבר מ/אל "none) – מכיוון שצריך לעדכן את הבנים
  • float (מעבר מ/אל "none")

סיכום

הנה כמה דברים מעשיים שאתם יכולים לעשות בכדי לשפר את ביצועי אפליקציית הווב שלכם:

  1. הדפדפנים כיום כוללים כלי פיתוח מתקדמים, בעיקר כרום ופיירפוקס. מומלץ ללמוד לעבוד עם Chrome Developer Tools ו/או Firefox Developer Tools. כאשר תשפרו ביצועים – רוב הזמן שלכם יילך לשם.
  2. כלים כמו YSlow או Page Speed יתנו לכם ניתוח מהיר, בעיקר על ביצועי הרשת.
  3. נסו להקדים את טעינת ה CSS להתחלה, ואת הרצת הסקריפטים לשלב מאוחר יותר.
  4. זכרו בעת כתיבת CSS שאת החוקים הדפדפן מנתח מימין לשמאל, ונסו לשים בצד ימין תנאי שקל לשלול.
  5. נסו לצמצם את מספר החוקים ב CSS ואת עומק ה DOM – במידת האפשר.
  6. היו מודעים ל DOM Tree Write buffer וכתבו קוד כך שלא יבצע flush ללא סיבה.
  7. למדו אילו פעולות גורמות לעבודה ב Rendering Engine ונסו להשתמש בפעולות שלהן יש דרכי קיצור.
  8. קראו את הפוסט על מנוע הג'אווהסקריפט – הוא כולל כמה טיפים נוספים.

שיהיה בהצלחה!

—–

[א] כלל זה עובד מצויין, יש לשים לב שאת קבצי ה CSS אנחנו רוצים לטעון מוקדם ככל הניתן (ז.א. ב HEAD) מכיוון של הוספה של Style Rule היא יקרה ביותר, במיוחד ככל שה DOM Tree הולך וגדל.

מקורות נוספים:

Faster HTML and CSS

הרצאה טובה מאוד של דויד ברון (עובד עבור מוזילה) על תהליך והרינדור והאפטימיזציות השונות.
http://www.youtube.com/watch?v=a2_6bGNZ7bA

צמצום פעולות ה Re-layout וה Paint
כולל דוגמאות קוד.
http://www.phpied.com/rendering-repaint-reflowrelayout-restyle/

קורס מזורז לביצועי ווב
מצגת טובה!
http://www.igvita.com/slides/2012/webperf-crash-course.pdf