על Performance Monitoring ו New Relic

New Relic (בקיצור NR) הוא כלי ה Application Performance Monitoring (בקיצור APM) מהמוערכים שזמינים בשוק היום.
Relic הוא שריד קדום, והשם New Relic נבחר כמעט כבדרך אגב: היזם (Lew Cirne) השתעשע בתוכנה שמוצאת צמדי-מלים המורכבות מהאותיות של שם המשתמש, ובעת רישום החברה זה השם שנבחר, כמעט באקראיות. מאז הוא נותר.NR הוא לא יחיד: יש את AppDynamics (שמכוון יותר ל Enterprise), או Nagios (פתרון Open Source) נפוץ למדי – ויש עוד רבים.

NR איננו זול: הוא עולה כ 100-150$ ל host לחודש. אם מנטרים כמה עשרות שרתים יכולים להגיע בקלות לכמה אלפי דולרים בחודש, אבל אנחנו (כמו עוד לקוחות רבים של פתרונות ה Premium) – החלטנו שזו השקעה משתלמת.

אנו ב Gett משתמשים ב NewRelic, ובכלי משלים בשם Graphite, שהוא יותר תשתית ל"הרכבה עצמית" של monitoring. יש גם את StackDriver. דיי נפוץ לראות ארגונים שמשתמשים ביותר מכלי אחד ל APM.

ל APM יש שני שימושים עיקריים:

ניטור בעיות ב production בזמן אמת
הרבה תקלות טכניות ניתן לזהות ע"י מדידת שיוניים בביצועי המערכת.
לעתים, לא נזרקות הודעות שגיאה חריגות, ושום Alerts מרכזי שכיוונתם ואתם מנטרים לא מזהה משהו חריג – אבל משהו רע קורה למערכת. לפני שהתקלה ממש משפיעה על המשתמשים – יש סימנים מקדימים: לעתים אלה יהיו spikes של עומס על המערכת, ולעתים דווקא ירידת עומס בלתי מוסברת: כמו צונאמי שמתחיל בירידת מפלס הים… וקצת אח"כ הוא מכה.
אם העסק שלכם מבוסס על זמינות המערכת, בוודאי תרצו לאתר את הסימנים המקדימים הללו – ולהגיב בהקדם.
שיפור ביצועי המערכת
מדי פעם, מחליטים שהגיע הזמן לשפר ביצועים. אולי בגלל feature חדש שמעמיס על המערכת מעבר לצפוי, ולעתים בגלל שהמערכת כבר "לא סוחבת" כפי שסחבה פעם (או שסתם נמאס לכם לשלם עשרות אלפי דולרים בחודש על Infrastructure).
NR (וכלים דומים) יכולים לספק במהירות ובקלות ניתוחים התחלתיים טובים לצווארי הבקבוק העיקריים במערכת. לפעמים הניתוח של NR כמו שהוא – מספיק בכדי להבין את השיפור המדויק שיש לבצע  (למשל: שאילתת SQL יעילה יותר), לעתים אחרות NR מצביע על אזור הבעיה, ואז מתחילים באיטרציות של ניסויים / הוספת monitors (לעתים ב new relic, ולעתים בכלים קרובים יותר לקוד) – עד לאיתור המדויק של הבעיה ומציאת הפתרון.

במהלך הפוסט אתמקד ב New Relic, אבל רבים מהעקרונות שאציג – נכונים לכלים רבים אחרים.

הטריגר לפוסט, אגב, הוא כוננות "Tier 1" שעשיתי השבוע. זו כוננות ב Gett לקבל טלפון אם יש Crisis במערכת, באחת מהמדינות, לבצע ניטור ראשוני ולהעיר את הצוות שיטפל במשבר. New Relic הוא כנראה המקום הראשון שאבדוק בו, על מה המהומה. בלילות כאלו – זהו חבר קרוב.

צורת העבודה של New Relic

New Relic תומך בסביבות קוד מסוימות:

  • רובי
  • ג'אווה
  • NET.
  • פייטון
  • PHP
  • node.js

בכדי לחבר את השרת שלכם ל new relic יהיה עליכם להטמיע ספרייה קטנה בקוד שתאסוף נתונים מתוך האפליקציה ותשלח אותם לשרתים של NR. NR הוא פתרון SaaS.

על כל שרת שאנו מנטרים בעזרת NR, מתקינים agent. ה agent הזה יודע לאסוף נתונים מתוך מערכת ההפעלה, מתוך הקוד שלנו (שלו הוספנו את הספרייה הקטנה של NR) ועוד סדרה של שרתים – בעזרת מערכת ה Plugins של NR. ישנם Plugins לעשרות אפליקציות מעניינות שנרצה לנטר: שרתי ווב (כמו nginx), בסיסי נתונים, memcached, רדיס, AWS, ועוד

בכדי לצמצם את ההשפעה של ה agent על השרת עצמו, NR מתמקדת באיסוף נתונים בחשיבת עלות (ערך לניתוח ביצועים) / תועלת (ההשפעה על המערכת באיסוף שלהם). כמו כן ה agent מפחית את ההשפעה שלו על השרת בו הוא רץ בכך שהוא אוסף buffer של נתונים ושולח אותם כ batch פעם בדקה (זו החלטה סבירה מכיוון שהניתוח עצמו נעשה כמה דקות אחורה, ולא באמת ב realtime). שליחת Alerts מה Agent, כמובן – מתבצעת בו במקום.

השרתים של NR אוספים את כל הנתונים שה agents שולחים ומבצעים קורולציה (תיאום) ביניהם – לתמונה אחת ואחידה.

חלק מה"קסם" שגרם ל New Relic להיות מאוד פופולארי הוא בהצגת מדדים שימושיים (ולא סתם spam של נתונים) בצורה מאוד נוחה לגישה, ומאוד אינטואטיבית. מרגע שאני מחבר את השרתים שלי ל NR – אני יכול לעבוד מייד, ויש לי סט מסודר והרמוני של כלים ותצוגות לעבוד איתו.

למשל: בצפייה ב Dashboard הראשי, ניתן ללחוץ על כפתור שמציג את הנתונים הנוכחיים, מול נתונים באותה השעה אתמול, ומול נתונים באותה השעה לפני שבוע.

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

זהו Tradeoff בין פשטות ושימושיות, על חשבון יכולת להתאמה אישית.

NR יודע לנטר גם זמן רנדור של אפליקציות ווב בדפדפן ואפילו אפליקציות Native Mobile. יש גם מוצר ל Analytics עמוקים יותר על הנתונים הסטטיסטיים, בשם "Insights". למרות הכל – בפוסט זה אתמקד רק במוצר ה APM לניתוח נתוני ביצועים מהשרת.

על מדד ה Apdex

מדד ה Apdex (קיצור של Application Performance Index), הוא מדד שמזוהה מאוד עם NR, מכיוון שזו עושה בו שימוש אינטנסיבי במוצר.
Apdex לא הומצא ע"י NR, אך בגלל השימוש הנרחב שלו במוצר, כדאי מאוד להבין מה הוא מתאר וכיצד הוא מתנהג.
עצם הרעיון של Apdex נובע מהחסרונות המובנים של מדדים כגון ממוצע או Median. מדדים שכאלו יכולים להסתיר בקלות התנהגויות חריגות ובעייתיות במערכת.

הנה, למשל, ארבעה קבוצות של נתונים שלכולם מספר איברים זהה, ממוצע זהה, שונות זהה ו median זהה – אך הם מתארים התנהגויות שונות לחלוטין:

כאשר אנו "מסכמים" את ההתנהגות תחת מדדים כמו ממוצע או Variance – אנו מאבדים מידע חשוב.

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

מדוע "Apdex עובד"? – אני לא יודע להסבר. אך הוא נחשב מדד מוצלח.
כמובן שגם הוא לא מושלם.

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

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

מדד Appdex עובד כך:
עבור כל שרת (או טרנזקציה מרכזית) אנו מגדירים את הזמן הממוצע שאנו מצפים מהשרת להגיב. זוהי "נקודת הייחוס", והיא מסומנת כ Apdex-T.

  • כל טרנזקציה שתסתיים בזמן שהוגדר או מהר יותר – תחשב ל"משביעת רצון" מבחינת הביצועים. (בירוק בתרשים למעלה)
  • כל טרנזקציה שתסתיים בטווח שהוא בין הזמן שהוגדר כ"משביע רצון", עד לפי-4 מכך – תחשב כ "נסבלת" מבחינת ביצועים (בכתום / ירוק זית – בתרשים למעלה).
  • כל טרנזקציה ארוכה מכך, או טרנזקציה שנסתיימה בשגיאה (למשל: HTTP 500) – תחשב ל "מתסכלת".
מדד Apdex הוא אחוז הפעמים בהן הגענו לטרנזקציות "משביעות רצון" ועוד חצי מהפעמים (משקל פחות) בהן הגענו לטרנזקציות "נסבלות". טווחי הערכים של Apdex נעים בין 0.0 (אסון) ל 1.0 (מעולה)הנה דוגמה:

בשרת הזה אנו מצפים לביצוע טרנזקציה תוך 125ms, ומקבלים זאת ברוב הזמן.
מדד של 0.87 הוא טוב – אך לא מעולה. ניתן לשאוף לקצת יותר.

במדידות הדפדפן (NR מסמלצת גם קריאות מדפדפן לשרת – בכדי לבדוק את זמן התגובה של דף באתר, כולל הרינדור) הגדרנו נקודת ייחוס של 12 שניות (פשוט לא הגדרנו נקודת ייחוס) – ולכן אנו נמצאים ב Apdex 1.0 עגול. מעולה! ;-).

במעבר עם העכבר על הגרף אני נחשף למידע נוסף, ויכול לראות שהתנודות ב Apdex שלנו נעו בין 0.84 ל 0.92 בשש שעות האחרונות (נקודת החיתוך) – יציבות סבירה לכל הדעות.

rpm (קיצור של requests per minutes) הוא המקבילה של tps המקובל יותר (transactions per minute) ואנו כרגע עומדים (אני רואה במעבר עם העכבר) על כ 8.43k rpm 130tps, או כ 140tps, עם מגמה קלה של עליה (יש בד"כ מחזוריות ברורה, יומית ושבועית, לשימוש בשירותים)

מדדים עיקריים ש New Relic מספק

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

הנה ה Dashboard העיקרי של NR:

  1. זהו הגרף הראשי המציג "לאן הלך הזמן במערכת". הוא מציג את הממוצע של הזמן שהושקע בבסיס הנתונים, ה Application Server, או קוד האפליקציה שלנו (במקרה הזה – רובי). בגלל ש NR מכיר את ריילס, הוא מסוגל לבצע חיתוך ולהראות לנו כמה זמן הושקע ב ActiveRecords (ספריית ה ORM של Ruby on Rails).
    במבט חטוף ניתן לראות שזהו שירות שמשתמש בתכיפות בבסיס הנתונים (צהוב) – אך יש לו גם הרבה עבודת CPU (תכלת).
  2. זהו הגרף שהצגתי קודם לכן, של Apdex ו Throughput.
  3. זוהי רשימת הטרנזקציות היקרות ביותר (שקלול של זמן ביצוע הטרנזקציה x שכיחות הטרנזקציה). אם נרצה לבצע שיפורי ביצועים בשרת – זה המקום להתחיל בו.
  4. Error rate של השרת – כמות הטרנזקציות שלא הסתיימו כשורה.
  5. רשימת ה alerts שעלו מהמערכת.
  6. רשימת השרתים שב cluster. ניתן לראות CPU, disk-usage, צריכת זכרון ו Apdex – לכל שרת בנפרד.
כשאנו רוצים לשפר ביצועים, הכי הגיוני יהיה להתמקד בטרנזקציות שגוזלות הכי הרבה זמן (משוקלל). ניתן לבחור טרנזקיות ע"פ מדדים שונים (הכי אטיות, גוזלות הכי הרבה זמן, בעלות Apdex הנמוך ביותר, וכו') – ואך לעשות Drill down לטרנזקציה:

  1. אנו יכולים לראות את השונות בזמני התגובה של הטרנזקציות. האם יש הרבה אטיות / מהירות במיוחד – או שיש סוג של זמן קבוע שמסביבו כולן סבות? (צהוב – האחוזון ה 95% האטי, אדום – ה media, ירוק – הממוצע)
  2. לאן הולך הזמן בטרנזקציה: GC, קוד, בסיס נתונים, מערכות 3rd Party וכו'.
  3. כמה טרנזקציות לדוגמה (לרוב מהאזור הפחות טוב). הנה טרנזקציה שלקחה 1.3 שניות.
  4. למרות שמפתה למדוד את הטרנזקציה הגרועה ביותר (#3), לרוב זהו מקרה קצה שעלול להטעות (למשל: בדיוק היה אירוע Full GC).
    אני מעדיף לבחור את השורה השלישית, שהיא קצת יותר נורמטיבית – ולחקור אותה. להזכיר: הזמנים המדוברים הם Wall time clock – וזמן בו הקוד נמצא ב block גם הוא נספר.
והנה כבר ה Trace ש NR אוספת על הטרנזקציה:
לעתים ניתן לזהות מתוך ה trace את הבעיה. בעיות נפוצות הן:

  • "בעיית n+1" – בה יש קשר master-detail בבסיס הנתונים, עושים שאילתה אחת לאובייקט האב ועוד n שאילתות – אחת לכל אובייקט בן (במקום שאילתה אחת עם inner join)
  • שאילתה בודדת יקרה ביותר – ניתן ללחוץ על שורה ולראות את השאילתה הקונקרטית שבוצעה.
  • בעיית קוד (מתאפיינת לעתים קרובות ב GC גבוה).
בתמונה למעלה ניתן לקראות דוגמה שנראית כמו בעיית קוד – 2 פעולות "תשתית" לכאורה, שכל אחת לוקחת יותר מ 300ms. סימן השאלה מסביר ש NR לא עשתה Drill down ומנחה מה לעשות הלאה.

Drill down לתוך הקוד NR בחר במודע לא לעשות – כדי לא להשפיע לרעה על ביצועי האפליקציה. Drill Down שכזה עשוי להיות יקר בצורה מטרידה. במקום זאת, ניתן להוסיף לקוד custom monitors. למשל, הוספת הפקודה הבאה בקוד:

add_method_tracer  :my_method  'Custom/MyClass::my_method'

בתוך המחלקה MyClass תיעזר בספריה של NR שהוספנו לקוד ותבצע רישום זמני כניסה ויציאה מהמתודה my_method בה אני חושד שאיננה יעילה. הרישומים יופיעו כמובן ב NR רגעים מספר מרגע העדכון.

בכדי לא לבצע deploy לכל המערכת, ניתן בקלות יחסית (לפחות ברובי) לכתוב קוד ש"יפתח" ו"יסגור" monitoring על מתודות מסוימות ע"פ קונפיגורציה שנשלחת מרחוק. ניתוחים זריזים שכאלו יכולים להיות בעלי חשיבות רבה מ-2 סיבות:

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

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

עוד יכולות מעניינות (בקיצור)

Developer Mode

ה Developer Mode הוא גרסה רזה של NR שניתן להפעיל On-Premises – על מכונת הפיתוח, ויכולה לשמש לצורך ניטור ראשוני של הקוד לפני שהוא מגיע ל production. למשל, Queries בעייתיים ניתן לאתר בשלב מוקדם כבר כך – ולפני שמגיעים ל production. כמובן שהתנהגות המערכת ב production ועל מחשב של מפתח היא שונה – ולא כדאי לנסות להסיק יותר מדי מתוך ה Developer Mode.

ניתן להפעיל את ה Developer Mode מתוך קובץ הקונפיגורציה, newrelic.yml, ע"י הפיכת הערך developer_mode ל true – ואז יהיה ניתן לגשת ל dashboard דרך http://localhost:3000/newrelic.
ה Dashboard של ה Developer Mode הוא הרבה פחות אטרקטיבי ועשיר – אבל הוא מכיל את נתוני הבסיס החשובים על הטרנזקציות.

דו"חות מוכנים

ל NR יש סדרה של דוחות מגניבים – חלקם אפילו שימושיים! למשל:

  • חישוב ה up-time של המערכת (למשל: 99.9734%) בתקופת זמן נתונה.
  • בניית גרף ה Scalability של המערכת – עלייה בזמני התגובה של השרת ככל שמספר הבקשות גדל (ואולי יש עוד nodes ב cluster בכדי לשרת). גרף זה עוזר לזהות צווארי בקבוק פוטנציאליים.
  • Speed Index – השוואה של הנתונים של השרת שלנו מול אתרים אחרים בתעשייה (לא ראיתי כיצד זה יכול להיות מועיל)
  • מיפוי ויזואלי של התקשורת העיקרית בין השרתים שלכם.
ניתוח אוטומטי של NR על תלות בין כמה שרתים שלנו
Insights
את המידע העצום ש NR אוספת על השרתים שלכם ניתן לתחקר באופן חופשי (יחסית) ובעזרת שפת NRQL (שפה דומה ל SQL) – על מנת לבצע ניתוחים ש NR לא מספקת "Out of the box"

סיכום

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

למרות הכיף והנוחות ש New Relic מספק, ניתוח ביצועים של מערכת היא עדיין אחת המטלות שאני מרגיש שבהם אני מנצל את מיטב המשאבים שלי: ללא ידע עמוק על המערכת, ידע תאורטי טוב במערכות הפעלה / רשת / סביבת הריצה הספציפית, וקצת חשיבה מקורית – קשה להגיע בניתוח ביצועים לתוצאות איכותיות.

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

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

—–

קישורים מעניינים

הפלטפורמה של NR (מתוך הבלוג הרשמי)
NR ב highscalability.com (פוסט מ 2011)

Crash Course ב NR (וידאו של חצי שעה). מוצלח יותר מרוב החומרים שמצאתי בנושא ברשת
New Relic vs. AppDynamics (פוסט מבית טאקיפי)

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

שפת ג׳אווהסקריפט תוכננה כשפה פשוטה למדי, שפה מפורשת המורצת ע״י מפרשן (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

ביצועים של אפליקציות ווב: הרשת

לשעבר: "המדריך לטרמפיסט: הצד האפל של ביצועי אפליקציות ווב".

בפוסט הקודם, ביצועים של אפליקציות ווב: מבוא לצד האפל, התבוננו בהשפעה המשמעותית של הרשת על ביצועי אפליקציות הווב. ראינו ש ל-Bandwidth כיום חשיבות פחותה, ושצוואר הבקבוק האמיתי הוא ה-Latency – משהו שמאוד קשה לשפר ברמת התשתיות. כמו כן ראינו שהמצב חמור יותר ברשתות 3G של מובייל, שם ה Latency לעתים כפול מזה של הרשת הקווית.

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

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

מקביליות בטעינת Resources

כפי שראינו בפוסט הקודם, טעינה של אפליקציית ווב מורכבת מעשרות, אם לא מאות, קריאות HTTP.
מדוע, בעצם, יש כ"כ הרבה קריאות?

  1. אנו נוהגים לכתוב את האפליקציות בצורה מודולרית ולחלק את הקוד להרבה קובצי javaScript ו CSS.
  2. אנו משתמשים בספריות עזר, שכוללות עוד קבצי javaScript ו CSS.
  3. אנו משתמשים בהרבה תמונות: קצבי jpeg, png וכו'
  4. לעתים משתמשים ב Template Engines ואז מייצגים כל Template כקובץ HTML נפרד.
  5. אנו מוסיפים לאתרים / אפליקציות כפתורי שיתוף סוציאליים (Facebook Like או "1+"), ווידג'טים (כמו Outbrains) או כלי אנליטקס – כל אחד הוא לרוב קובץ javaScript וקובץ CSS.
  6. אנו מבצעים קריאות Ajax לשרת(ים) בכדי להביא מידע.
  7. פרסומות

ועוד….

ניתוח בעזרת (Firebug (FF Plugin של טעינה של הדף הראשי באתר Ynet. מעל 350 קריאות לשרת.

אתר Ynet הוא דוגמה קצת קיצונית אך אמיתית לגמרי. למרות ריבוי הקריאות, האתר נטען בזמן נסבל (11 שניות) כשה-cache קר [א]. זמן "סביר" זה הוא בזכות Latency נהדר ש Ynet נהנה ממנו: כ 20-30ms בחיבור מספק כבלים בישראל. גישה ממדינה אחרת הייתה מספקת חוויה אחרת לגמרי.
ייתכן (אני לא יודע) ש Ynet משתמשים בשירותי CDN על מנת לשמור על Latency כ"כ טוב. לעתים יותר זול לשכור שירותי CDN מאשר לשנות את הקוד של האתר למבנה יותר אופטימלי. זו החלטה עסקית לגיטימית.
כשיש לנו כ +300 קריאות HTTP, דיי ברור שמיקבול הקריאות יכול לגרום לשיפור מאוד משמעותי. רוב הדפדפנים המודרנים פותחים במקביל כ 6 TCP Connections ל host על מנת להוריד קבצים (6 לכל הטאבים ביחד, אם אני זוכר נכון).

מדוע, אם כן, לא לפתוח 10, 20 אולי אפילו 40 connections מקביליים?

סיבה ראשונה: הסכנה להעמיס על השרתים יתר על המידה ולגרום בלי כוונה ל Denial of Service. שרתים קלאסיים (למשל JEE) דורשים משאבים רבים לכל connection שמוחזק פתוח. אם הדפדפנים יפתחו עשרות connections מול כל שרת, הם עלולים להשבית שרתים רבים. ע"פ תקן ה HTTP (מקור: RFC2616, עמ' 46) אין לפתוח יותר מ 2 connections במקביל ל Host יחיד – כלל שכל הדפדפנים כבר היום חורגים ממנו.

סיבה שנייה: (ואולי יותר חשובה) הצורה בה עובד פרוטוקול TCP.
לפרוטוקול TCP יש מנגנון שנקרא TCP Congestion Control, או בצורה יותר עממית: "Slow Start" ("התחל חלש"). ל TCP אין שום מידע על מהירות החיבור של הלקוח (כלומר מה ה Bandwidth הפנוי אליו) ועל כן הוא מתחיל לאט, ומעלה את הקצב בהדרגה. על כל הודעה שהוא שולח הוא מצפה לאישור (הודעת acknowledge). ברגע שהוא מפסיק לקבל אישורים על אחוז מסוים מההודעות, הוא מניח שהוא הגיע ל Bandwidth המרבי ומייצב את קצב ההעברה [ב]. במילים אחרות, ניתן לומר של TCP Connection לוקח כמה שניות "להתחמם" ולהוריד מידע בקצב מרבי.
״התחממות״ שאורכת מספר שניות איננה בעיה בהורדת קובץ של כמה GB – שאורכת כשעה, אבל זו בעיה כאשר רוצים לטעון אתר בשניות בודדות: פתיחת 40 connections מקביליים משמעה 40 connection שלא יספיקו ״להתחמם״ ולנצל את ה Bandwidth האמיתי שקיים.

טקטיקה: מקביליות בעזרת ריבוי Hostnames

טכניקה אחת לשיפור זמני הטעינה של אתר האינטרנט היא לחלק את הקבצים של האתר לכמה hosts שונים. לדוגמה:
yent.co.il ו images1.ynet.co.il, על מנת לגרום לדפדפן לפתוח יותר TCP connections במקביל: נאמר 12 במקום 6.
כפי שכבר הבנו, "תכסיס" זה יוצר trade-off בין מקביליות, למהירות ה TCP Connections.

מקובל כיום להאמין ש 6 TCP Connections היא נקודת האיזון האופטימלית בין השניים. טכניקה של ריבוי Hostnames הייתה בעלת משמעות בתקופה שדפדפנים פתחו רק 2-3 connections ל Host יחיד. כיום, הדפדפנים כבר התיישרו לנקודת 6 ה connections ל Host, ועל כן טכניקת ריבוי ה hostnames נחשבת כמיותרת ולרוב לא יעילה – שלא לדבר על הסרבול שבמימוש שלה.

כדאי להזכיר שוב את החשיבות של מנגנון ה "HTTP "Keep-Alive באספקט זה: שימור TCP connection לא רק חוסך את ה Three way handshake, אלא גם שומר על ה Connection "חם".

דפדפנים מסוימים שומרים את ה TCP Connections פתוחים עוד כמה שניות בכדי לא "לאבד" Connection "חם" בציפייה לקריאת Ajax שעוד מעט תבוא.

טכניקה הופכית לטכניקה הנ"ל, היא לארח באתר שלכם כמה Scripts ו CSS של ספריות חיצוניות, במקום להפנות לאתר המקורי שמארח אותן וכן להינות מה Connections ה"חמים".
גם כאן יש Trade-off לא טריוואלי: נניח ואתם בוחרים לארח קובץ מאוד נפוץ, לדוגמה ה Script של כפתור ה "Like" של פייסבוק: במקום לקרוא את הקובץ מ

אתם מביאים אותו מ:

נוצרת בעיה: ה browser cache עובד ע"פ absolute URI ולכן במקום לקחת את הקובץ שכבר נמצא ב cache ע"פ ה URI של פייסבוק, הדפדפן יטען את הקובץ מחדש מהאתר שלכם. זכרו: ה roundtrip הכי זול הוא ה roundtrip שלא קרה.

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

טקטיקה: Minification

טכניקה נפוצה למדי לשיפור ביצועים של אפליקציות ווב היא "לדחוס" את קובצי ה javaScript ו/או CSS, בתהליך שנקרא minification, crunching או לעתים אף uglification :). קבצים קטנים יותר –> נוכל להעביר קבצים יותר מהר ב 6 ה Connections שלנו.

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

לדוגמה, קטע קוד הבא:

מצומצם ל:

קבצי javaScript נדחסים לרוב בין 50% ל 90% (כאשר יש המון הערות). קבצי CSS נדחסים לרוב בין 30% ל 50%.

לזכות טכניקת ה minification ניתן לומר שהיא פשוטה ליישום: פשוט מוסיפים עוד שלב ב build שיידחוס את הקבצים.
הכלים הם רבים וסטנדרטיים: YUI Compressor של יאהו!, Closure Compiler של גוגל או CSSO לדחיסת קבצי CSS.
דחיסת קבצי javaScript מסוגלת לעתים להסיר קוד שלא בשימוש, וכך לחסוך זמן CPU של הדפדפן על פענוח וטעינת קוד JavaScript מיותר.

קושי
נניח שהכנסנו כלי Minification לתהליך ה Build שלנו וראינו שיפור ביצועים – נהדר. הבעיה: מה קורה כאשר אנו רוצים לבצע Debug?
כמעט ובלתי אפשרי לבצע debug לקוד שהוא minified – הוא פשוט לא קריא.

פיתרון אפשרי אחד הוא לזהות בצד-השרת את מצב ה-debug, ואז במקום לטעון את הקבצים ה minified – לטעון את הקבצים המלאים (שאינם minified). כל טכנולוגיה עושה זאת בצורה קצת אחרת – אך יש פה תקורה למפתחים.
דרך קצת יותר מודרנית היא להשתמש ב Source Maps – קבצים שמכילים את קוד המקור עם המיפוי לקוד ה minified כך שהדפדפן מריץ את הקוד ה Minified אך מציג לכם את קוד המקור. Source Maps יעילים גם במקרים שקוד המקור שונה מהותית מהקוד שרץ, לדוגמה כאשר כותבים ב"שפות-על" כמו LESS או CoffeeScript.
Closure Compiler, מבית גוגל, מספק יכולת לייצר source maps תוך כדי תהליך ה minification.

שאלה לגיטימית לגבי השיטה היא עד כמה היא משמעותית בפעול לביצועי האתר / אפליקציה?
ע"פ ה HTTP Archive, פרויקט שעוקב ומנתח מגמות במבנה אתרים ברשת, החלק הארי של ה Bandwidth מושקע בהורדת תמונות. בכדי לשפר Bandwidth כנראה שיותר יעיל לדחוס טוב יותר את התמונות. ישנן דרכים רבות לעשות זאת: שינוי פורמט, עומק צבע, חוזק דחיסה, התאמת גודל-למכשיר ועוד – לא אנסה לכסות תחום רחב זה בפוסט הנוכחי.

 

To make a long story short:
כמובן שיש ערך לדחיסה נבונה יותר של התמונות, אך גם קובצי ה javaScript שווים את המאמץ:

  • Minification של javaScript יכול עדיין להיות משמעותי בהפחתת כמות המידע להורדה. בשנים האחרונות חלקם היחסי של קובצי ה javaScript הולך וגדל משמעותית.
  • יש ייתרון בחיסכון של זמן ה CPU לדפדפן ואפשור ל Scripts לרוץ מעט יותר מוקדם. ההשפעה של קובצי ה javaScript על ה Perceived Performance היא גדולה מחלקם היחסי ב"עוגת ההורדות".
לגבי דחיסה של קובצי CSS: אכן דחיסה זו היא פחות משמעותית, וניתן להחשיב אותה כ "Nice to have".
gzip

ישנה "אלטרנטיבה זולה" לצמצום ה Bandwidh ברמת התשתיות וללא כתיבת קוד: Gzip.
ניתן להגדיר אפשרות דחיסת gzip (וריאציה של zip שקלה מאוד לפענוח) בשרת המארח כך שידחוס את הקבצים לפני שהוא שולח אותם. כל הדפדפנים כיום יודעים לקבל ולפתוח פורמט זה, וכנראה רק בשל חוסר הכרות עם הנושא – אחוז גבוה של אתרים / אפליקציות לא משתמש ביכולת ה gzip.

האם יש חפיפה בין Minification ל gzip? במידה. בכל זאת משתלם עדיין להשתמש בשניהם. לדוגמה: gzip לעולם לא יסיר הערות בקוד ש Minification יסיר בקלות.

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

שרתי ווב (לדוגמה Apache) מאפשרים לציין לאילו סוגי קבצים לבצע דחיסה.
gzip הוא יעיל עבור כל פורמט טקסטואלי (כולל JSON או XML) אך אינו יעיל עבור קובצי תמונה (jpeg, png) או PDF – שהם כבר דחוסים. gzip לא יקטין אותם יותר.

טקטיקה: איחוד קבצים

נקודה #1 בשיפור ביצועי ווב היא צמצום מספר ה Roundtrips לשרת.
טכניקה יעילה למדי היא איחוד קבצים: פחות קבצים –> פחות Roundtrips.

בזמן תהליך ה build, אנו לוקחים את כל קובצי ה CSS שלנו (לעתים יש עשרות) ומאחדים אותם לקובץ אחד. מבחינת הדפדפן, חלוקה לקבצים היא שרירותית – ולכן אין שום בעיה לאחד אותם לקובץ אחד. כנ"ל לגבי קובצי javaScript.
רוב כלי ה Minification מסוגלים לבצע גם פעולה של איחוד קבצים.
באופן זה אנחנו יכולים לחסוך עשרות roundtrips לשרת ולשפר מאוד את זמן הטעינה.

ישנם 2 חסרונות גדולים לטכניקה זו:

  • אם נאחד את כל הקבצים, ייתכן ונוריד קבצים שלא נזקקים להם בתסריטים מסויימים = בזבוז ה Bandwidth.
  • הדפדפן מתחיל להריץ את קוד ה javaScript רק לאחר שביצע parsing לכולו. אם נוריד במכה אחת כמות גדולה של javaScript – ייקח זמן רב יותר עד שמשהו יחל לרוץ, מה שיפגע ב Perceived Performance. זו בעייה משמעותית באפליקציות גדולות ומורכבות.

לסיכום: טכניקה זו היא חשובה וטובה לקובצי CSS ו javaScript (וגם ל HTML Templates, אם אתם משתמשים בהן) אך יש לחשוב ולהרכיב "חבילות" בתבונה – דבר שדורש לא מעט התעסקות.

באתרים / אפליקציות רבות יש הרבה מאוד תמונות קטנות (אייקונים או כפתורים) מה שגרם לקהילה להרחיב טקטיקה של איחוד קבצים גם עבור תמונות – קבצים שבאופן טבעי לא נועדו לאיחוד. הנה 2 טכניקות מקובלות:

Sprites
Sprites הם "שדונים" ובטכניקה זו אנו מרכיבים הרבה תמונות קטנות לתוך קובץ אחד. באופן זה נוכל להוריד הרבה תמונות קטנות בעזרת Roundtrip אחד.

מקור: http://www.w3schools.com/css/css_image_sprites.asp

שפת CSS מאפשרת לנו להציג רק "צוהר" מהתמונה, באופן הבא:

  • אנו קובעים גודל נוקשה לתמונה שאנו מציגים (רוחב וגובה בפיקסלים).
  • אנו משייכים את קובץ ה sprites ל"צוהר" שהוגדר וקובעים את קורדינטות הכניסה (בדוגמה למעלה 0,0). אם היינו משנים את הקורדינטות ל 47- (x) ו 0 (y) – היינו מקבלים את החץ שמאלה.

טכניקת Sprites איננה משמשת רק לתמונות. ספריות כגון howler.js יאפשרו לכם ליצור "Audio Sprite" – קובץ אודיו אחד שמאחד הרבה קטעי קול קצרים (למשל: צליל לחיצה על כפתור או צליל החלפת דף) – ולהוריד אותם כקובץ יחיד, וכך לחסוך roundtrips.

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

Data URI
טכניקה קצת יותר חדשה ("HTML5 like") היא לבצע inlining של תמונות קטנות לתוך קובץ ה HTML/CSS עצמו. הנה דוגמה:

במקום לציין URI לתמונה ככתובת HTTP למקום בו מאוכסנת התמונה, אנו מכניסים את התמונה עצמה, מקודדת ב base64 לתוך האלמנט עצמו. ניתן לעשות זאת בתוך ה HTML עצמו (כמו בדוגמה למעלה), אך כמובן שעדיף לעשות זאת בתוך קובץ CSS.
הביטו בקוד למעלה: מזהים מה מצוייר בתמונה? תתאמצו!

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

  • באתר בו התמונות מתחלפות בקצב מהיר – אנו מאבדים גם את היכולת לבצע caching יעיל ברמת התמונה.
  • אם אנו רוצים להוסיף אותה תמונה ב Data URI במספר קבצי CSS – העברנו את אותו המידע מספר פעמים.

שורש הבעיה

מדוע צריך את כל המשחקים הללו? מה היה צריך לשנות על מנת שיהיה אפשר לכתוב קוד בצורה קלה ואלגנטית?
להחליף את הדפדפן? את השרת? את המתכנת? 😉

ובכן…הבעיה, כנראה נעוצה בפרוטוקול ה HTTP עצמו. HTTP מאפשר לבקש קבצים (resources) רק באופן סיריאלי: ביקשתי אחד, ואוכל לבקש את הבא רק לאחר שהקודם הגיע במלואו.
היה ניסיון לפתור בעיה זו ב HTTP/1.1 בעזרת מנגנון בשם Pipelining, אך מסיבות שלא אפרט כאן – המנגנון "לא סיפק את הסחורה".
ישנה תקינה לפרוטוקול HTTP/2.0 שמציעה פתרון שנראה מוצלח בהרבה, אך היא צפוייה להסתיים רק בשנת 2015, ועל כן סביר שבחמש השנים הבאות נמשיך לעשות "תרגילים" על מנת לשפר את ביצועי אפליקציות הווב שלנו.

סיכום

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

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

[א] Cache קר אומר שלא ניגשתי לאתר זמן-מה, וה cache לא מעודכן. עם Cache "חם" (גישה לאתר בשנית לאחר 10 שניות) זמן הטעינה ירד לקצת מעל 6 שניות. לא טוב – אבל כנראה סביר.
[ב] סיפור מעניין הוא שאלגוריתם זה כשל ברשתות אלחוטיות / סללוריות בהן אחוז הכשלים ה"טבעי" הוא גבוה יחסית. היה צורך "להתאים" את האלגוריתם לרשתות אלו על מנת שיעבדו בצורה נכונה.

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

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

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

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

פשט

כאשר אפליקציית ווב רצה, היא מבצעת קריאות לשרת. לרוב מתארים אינטראקציה זו בצורה הבאה:

צורה זו הייתה נכונה יחסית באותם ימים בהם השרת בנה דף HTML על כל פעולה של המשתמש – והדפדפן רק רינדר אותו. זו עדיין תמונה שיש לאנשים רבים בראש, למרות שהיא מאוד לא מדויקת.

הערה: כמובן שבעת שעוסקים ברמה של קישור בין מערכות, ב High Level – זו רמת הפשטה נאותה.

מהתרשים לעיל, ניתן להסיק על חלוקת העבודה כמשהו בנוסח הבא:

האנטומיה של קריאת HTTP

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

פרוטוקול HTTP הוא פרוטוקול אפליקטיבי – הוא שולח נוסח טקסט מוסכם מראש המתאר התנהגות אפליקטיבית על גבי פרוטוקול TCP.

פרוטוקול TCP – הוא פרוטוקול לניהול תקשורת (Transmission Control Protocol) שמנהל Connection לשליחת רצף של הודעות, דואג שכולן יגיעו ושיגיעו בסדר הנכון.
מי שעושה באמת את העבודה הוא פרוטוקול (IP (Internet Protocol שאחראי לשליחת הודעות, ללא הבטחת סדר וללא אמינות.

כל קריאה של פרוטוקול IP היא בעצם הודעה שעוברת ממחשב אחד (לקוח) למחשב אחר (שרת). למרות שנדמה מהתרשים לעיל שהייתה בעצם הודעה אחת ותשובה אחת בין הלקוח לשרת – בפועל, ברמת ה IP, היו הרבה קריאות ותשובות
.
הנה תיאור מפורט / מדויק יותר של מה שמתרחש בפועל:

תרשים 1: קריאת HTTP לשרת כפי שהיא מתבצעת בפועל
DNS Lookup
כשאנחנו מקלידים כתובת (למשל http://www.ynet.co.il) יש קודם כל ללמוד מה כתובת ה IP של שרת היעד לפני שאנו יכולים לשלוח לו הודעות. כתובות אלו הן דינמיות ועשויות להשתנות לאורך הזמן. תהליך ההפיכה של הכתובת הטקסטואלית (hostname) לכתובת IP היא תהליך שנקרא DNS Lookup. בפועל הדפדפן שלנו פונה לשרת (Domain Name Servers (DNS, שהוא קורא לסדרה של שרתים אחרים, כל אחד בתורו, בכדי לפרש את הכתובת. תחילה קוראים לשרת ה ROOT כדי ללמוד על כתובת ה IP של שרת ה "il" (מפרשים את כתובת ה hostname מהסוף להתחלה). בשלב הבא עלינו לפנות לשרת "il" וללמוד ממנו את כתובת ה IP של שרת ה "co" (קיצור של commercial – הרי ynet הוא אתר מסחרי) ורק אז מקבלים את הכתובת ה IP של השרת של YNET בכבודו ובעצמו.
תהליך ה DNS Lookup לוקח בממוצע 130ms (עבור כל השרתים שבדרך). התקשורת נעשית באמצעות פרוטוקול UDP שהוא מהיר יותר מ TCP – אך איננו אמין. הסטטיסטיקה אומרת שבכ 5% מתהליכי ה DNS Lookup הודעה כלשהי הולכת לאיבוד. על הדפדפן לזהות מצב זה (ע"י קביעת timeout) ולשלוח הודעה מחדש – מה שיכול לגרום לתהליך ה DNS Lookup להתעכב זמן רב (עד כשתיים-שלוש שניות). כל זאת לפני שעוד בכלל ביצענו קריאה לשרת שלנו.
Establish TCP Connection
יצירה של TCP Connection כוללת תהליך שנקרא "לחיצת יד משולשת" שבמהלכו מתבצעות 3 קריאות של פרוטוקול IP רק כדי להסכים על ה connection. רק לאחר 3 קריאות אלו ניתן בעצם לשלוח את הודעת ה HTTP. בתקשורת מאובטחת (קרי HTTPS) יצירת ה connection מערבת "לחיצת ידיים" ארוכה בהרבה.
תיאור סכמטי של ה Three Way Handshake
HTTP Request and Response
רק לאחר שהוקם ה TCP Connection ניתן לשלוח הודעה אפליקטיבית (HTTP) לשרת. השרת מבצע את הקוד הנדרש (קורא לבסיס הנתונים או מבצע לוגיקה) ומחזיר את התשובה. זמן זה מסומן בתרשים 1 כ Server Time. יש לזכור שהשרת משקיע עבודה גם ביצירת ה TCP Connection ושליחת ההודעה עצמה – מה שמצוין בתרשים 1 כצבע הירוק ברקע.
ה HTTP Request הוא לרוב טקסט קצר למדי (כמה שורות בודדות) בעוד שהתשובה היא לרוב ארוכה למדי (קובץ HTML או קובץ JPG למשל) – ולכן אורכת זמן רב יותר.
נתון מעניין הוא ש "אינטרנט מהיר יותר" -כמעט ולא משפר גלישה רגילה באינטרנט. האינטרנט המהיר מאיץ רק את החלק של ה "HTTP Response" וכפי שניתן לראות בתרשים 1 – זהו חלק קטן מהזמן הכולל.
הערה: חיבור אינטרנט מהיר הוא כן משמעותי כאשר מדובר בהורדה של קבצים גדולים או צפייה בוידאו.
Rendering Time on the Browser
כאשר דף ה HTML מגיע לדפדפן, על הדפדפן לפענח את הפורמט (ולפעמים לתקן בו טעויות), לתרגם אותו למבנה שנקרא (DOM (Document Object Model ואז לרנדר ממנו את תצוגת הדף. תהליך דומה קורא לקבצי CSS ולקבצי javaScript. מנועי הרינדור של הדפדפנים השתפרו פלאים בשנים האחרונות – אך עדיין שלב זה גוזל זמן רב. זמני פעולה זו תלויים בחומרה של מחשב הלקוח, מנוע הרינדור של הדפדפן – וכמובן בדרך בה נכתב ה HTML / CSS / JavaScript.

Latency

התיאור שהשתמשנו בו עד עכשיו, של תקשורת IP או TCP כתקשורת של נקודה לנקודה, לקוח לשרת, איננו מדויק.
האינטרנט איננו רשת אחת, אלא מערך של רשתות שמחוברות בניהן בנתבים (Routers). מקור השם Internet הוא קיצור של inter-connected networks. בפועל כל הודעת IP מדלגת מרשת לרשת עד שהיא מגיעה לרשת של מחשב היעד – ושם היא מנותבת מקומית.
כל הודעה מנותבת באופן פרטני כך שייתכן שהודעות עוקבות ישלחו בכלל במסלולים שונים.
לקוח שולח הודעת IP, המנותבת על פני מספר רשתות עד שהיא מגיעה ליעד. התשובה (הודעת IP נוספת) מנותבת בדרך קצת אחרת.
הנה הרצה של פקודת "trace route", באמצעותה אני בוחן את הדרך שהודעת IP עוברת מהמחשב האישי שלי עד לשרת עליו מאוכסן הבלוג:
כפי שאתם יכולים לראות היו 17 תחנות בדרך. התחנה הראשונה, אגב, היא הראוטר הביתי שלי (הרשת הביתית היא עוד רשת). כל תחנה בדרך מוסיפה כמה מילי-שניות ובסה"כ זמן ה Round Trip, כלומר הודעת IP הלוך וחזור, היא בסביבות ה 100ms. זהו זמן דיי טוב שמאפיין את שרתי גוגל מחוץ לארה"ב (blogger שייך לגוגל).
לאתרים ללא "נוכחות" מקומית (כמו netflix או rottentomatoes) זמן ה (Round Trip (RTT הוא כ 200-300ms בד"כ, כלומר חצי מזה לכל כיוון. בשעות העומס, המספרים גבוהים יותר.
RTT של 250ms אינו נשמע מטריד – הרי זה זמן שבן-אנוש כמעט לא יכול להבחין בו. הבעיה היא כמובן במספר ה roundrtips הנדרשים על מנת לטעון דף אינטרנט מודרני. כיום, דף אינטרנט ממוצע מורכב מ 82 קבצים ו 1MB של מידע.
בצורה הנאיבית ביותר מדובר ב 82 x יצירת TCP connections ו 82 x בקשות HTTP (כאשר HTTP הוא פרוטוקול סינכרוני ולא ניתן להוציא בקשה חדשה לפני שהתשובה מהבקשה הקודמת חזרה) = 164 המתנות של רבע שנייה (כ 40 שניות) לא כולל DNS Lookup, זמן שרת, זמן רינדור בדפדפן וללא זמן הורדת הקבצים (= Bandwidth אינסופי).
מניסיון החיים, ברור שזה לא המצב בפועל. יש הרבה אופטימזציות ברמות השונות שמשפרות את נתוני הבסיס הבעייתיים. הנה כמה אופטימיזציות חשובות:
  • פרוטוקול HTTP 1.1 הוסיף מצב של keep-alive בו ניתן לבקש לא לסגור ולעשות שימוש חוזר ב TCP connection שנוצר. השיפור: כ 100%
  • דפדפנים החלו לפתוח מספר Connections מקבילים לשרת על מנת להאיץ את הורדת הקבצים. זה החל ב 2-3 connections לשרת והיום מגיע ל 6 ואף 8 connections לשרת. השיפור: עד 800%
  • רשתות Content Delivery Networks של שרתים הפזורים ברחבי העולם מחזיקים עותקים מקומיים של שרתים ששילמו על השירות – וכך מקצרים את ה latency בצורה משמעותית. השיפור: עד 1000%. במקרי קיצון: אפילו יותר. ניתן לקרוא על CDN בפוסט תקשורת: מיהו התוכי האמיתי של האינטרנט המהיר?
  • לדפדפנים יש cache בו הם שומרים קבצים אחרונים שנטענו מקומית על המחשב וכך חוסכים את כל הסעיפים הנ"ל.  ה cache יעיל החל מהגישה השנייה לאתר. השיפור: אדיר.
ויש עוד…
עדיין – יש הרבה שאתרים / אפליקציות ווב יכולות לעשות על מנת לשפר את הביצועים שלהם ולנצל טוב יותר את התשתיות הקיימות.
ל Latency יש השפעה רבה בהרבה על ביצועי אתרי אינטרנט מאשר ה Bandwidth.
הקשר בין זמן טעינה של דף אינטרנט כתלות ב RTT
לרוע המזל, שיפור של Bandwith הוא זול יחסית (יותר כבלים) בעוד שיפור ב Latency הוא יקר הרבה יותר: כיום התעבורה למרחקים נעשית במהירות האור, ונתיבי הכבלים מוגבלים מתוואי השטח. לדוגמה: את הכבלים התת-ימיים בין ארה״ב לאוסטרליה ניתן לקצר בכ 40% באורך, וכך לשפר את ה Latency בכ 40% – אך לצורך כך יש למתוח אותם בקו ישר, מה שיגרום לעלויות אסטרונומיות. לא פלא שספקי האינטרנט מתמקדים בפרסום ה Bandwidth שלהם ולא כ"כ ב Latency (אם כי גם הוא משתפר עם השנים).

ההיבט המובילי

מכשירי מובייל, מלבד מעבד חלש יותר ו bandwidth נמוך יותר, סובלים גם מ Latency גבוה יותר. כאשר עובדים ברשת 3G, על התקשורת לעבור כמה תאים סלולריים ותחנות ממסר לפני שהם מתחברים לרשת האינטרנט מה שיכול להוסיף כ 200ms ל RTT.
כפי שאתם אמורים כבר להבין, המשמעות היא כבדה מאוד – ועל כן אפליקציות מובייל משקיעות הרבה יותר באופטימיזציות, לעתים עד האסקטרים.
הבשורה הטובה היא שרשתות דור רביעי (LTE) לא רק משפרות משמעותית את ה Bandwidth אלא גם את ה Latency – החשוב יותר. מדובר על תוספת של כ 50ms ל RTT במקום כ 200ms – שיפור גדול מאוד.

סיכום

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