אבני הבניין של האינטרנט: הדפדפן

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

רכיבי הדפדפן

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

Networking
מודול זה אחראי לניהול ה TCP connections (כמה לפתוח, כמה זמן להחזיק,…), ציות לכללי פרוטוקול ה HTTP הקשורים לתקשורת, וסיפוק שירותים מתקדמים כגון תקן webSockets של HTML5. לדפדפנים שונים יש מימושים שונים למודול ה Networking, המשפיעים על אופי התקשורת. ל Chromium (כלומר דפדפן כרום) יש מודול שנקרא Chromium Network Stack ולספארי יש את CFNetwork.
דפדפנים מודרניים מיישמים אופטימיזציות רבות ברמת הרשת:

  • יצירת connection מוקדם (preconnection) גם מבלי שהמשתמש לחץ על לינק. סיבות אפשריות: משתמש עבר עם העכבר מעל לינק (סיכוי גבוה שהוא ילחץ) או דפים שהמשתמש כמעט תמיד ופותח כשהוא מפעיל את הדפדפן (לדוגמה ynet).
  • השארה של TCP connection פתוח עוד כמה שניות…. אתרים רבים מבצעים קריאות Ajax זמן קצר לאחר שהאתר נטען (סוג של Lazy Loading). ה Connection כבר "חם" וחבל לסגור אותו.
  • ניחוש איזה משאב כדאי לבקש מאיזה TCP connection פתוח. לדוגמה: דחייה של בקשה לטעינת תמונות בכמה מאות מילי-שניות, כי אולי תבוא בקשה לטעינת קובץ javaScript או CSS – שיש יתרונות רבים לטעון אותם מוקדם יותר.

Storage
מודול האכסון מנהל אכסון של Caches, של תקני HTML5 כגון Local Storage או ניהול של Cookies או Plugins. למודול זה יש עבודה רבה עבור הדפדפן, אך השפעה קטנה יחסית על מפתח הווב.

Graphics
מודול זה כולל בתוכו הפעלה של ספריות הגרפיקה של מערכת ההפעלה. הן לצורך רינדור פקדים "טבעיים" של מערכת ההפעלה (למשל DropDown List) או פונטים והן לצורך הפעלה של האצת חומרה [א] שתסייע למנוע הרינדור.

מנוע הרצת הג'אווהסקריפט (javaScript Engine, לשעבר javaScript Interpreter)
המודול האחראי להרצת ג׳אווהסקריפט, נעסוק בו בפוסט נפרד.

מנוע הרינדור (Rendering Engine), ידוע גם כ Layout Engine
המנוע שהופך דפי HTML לתמונה על גבי המסך, נעסוק בו בהמשך פוסט זה.

Browser Management Layer
שכבה המנהלת שירותים שונים של הדפדפן כגון:

  • Bookmarks
  • ניהול היסטורית גלישה
  • סנכרון ה Bookmarks מול שרת מרוחק (פיירפוקס, כרום ולאחרונה גם ספארי)
  • ניהול Plug-ins
  • התמודדות עם קריסות
  • וכו'
 
Browser Shell

בעצם שכבת ה UI, ה "פנים" של הדפדפן כפי שאנו מכירים אותו:

  • טאבים
  • כפתורי "קדימה", "אחורה", Home וכו'
  • תיבת הכתובת / חיפוש וכיצד הן מתנהגות
  • למשל: הקלדה של כתובת ב Chrome או לחיצה על כפתור "refresh" תגרום לטעינה לדף בהתעלמות מה cache בעוד שטעינת דף בעקבות לחיצה על link תגרום לטעינת הדף תוך שימוש ב cache. התנהגות זו היא חלק מהגדרת ממשק המשתמש.
  • Viewers שונים, לדוגמה הצגת תמונות או PDF הן לרוב חלק מה Shell ולא ממנוע הרינדור.

מספר לא מבוטל של דפדפנים (maxthon, iCabMobile ולאחרונה גם אופרה) הם וריאציה של דפדפן אחר ומחדשים בעיקר ב Shell ובשכבת הניהול.

ה Shell של פיירפוקס, כפי שהוא נראה על מכשירים שונים.

מנוע הרינדור

אלמנט מרכזי מאוד בעבודת הדפדפן היא עבודתו של מנוע הרינדור (ידוע כ Rendering Engine או Layout Engine). מנוע הרינדור מקבל קובץ HTML וקובצי javaScript, CSS והופך אותם לגרפיקה על המסך.
יש הרבה מנועי רינדור, הנה הרשימה של כמה מהנפוצים שבהם:

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

פוסט זה מתבסס בעיקר על ההתנהגות הידועה של Firefox ו Chrome.

ה flow המרכזי של מנוע הרינדור הוא כדלהלן:

הדברים מסתבכים כאשר קוד javaScript מעורב. בפוסט זה אתמקד ברינדור של HTML סטטי – מכיוון שזה הבסיס, ובפוסט המשך אתאר כיצד javaScript נכנס לתמונה ליצירת HTML דינמי – תמונה המשקפת את המציאות של Web Applications כיום.

שלבי הטעינה


שלב 1: פענוח קובץ ה HTML ובניית ייצוג היררכי שלו בזיכרון (מבנה נתונים הנקרא DOM Tree)
הנה תרשים המתאר את פעולת הפענוח של קובץ ה HTML. הלוגיקה כוללת נקודות רבות של "התאוששות מתקלות" (שלא באות לביטוי בתרשים למטה) כגון תגיות שלא נסגרו / לא נסגרו בסדר הנכון או הימצאותם של אלמנטים לא תקניים ב markup:

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

שלב 2: טעינת קבצי CSS
כאשר נתקלים ב DOM באלמנטים של Sytle (קובצי CSS) – מבקשים מרכיב הרשת לטעון אותם. כשהטעינה מסתיימת – תוכן הקובץ נשלח לפענוח.

שלב 3: פענוח קבצי ה CSS
קבצי ה CSS מפוענחים בצורה דומה ל HTML (אם כי התהליך פשוט יותר) ומהם נבנה מבנה נתונים המחזיק את כל החוקים המתוארים בקבצי ה CSS, מבנה הידוע לרוב כ Style Tree או Style Rules. ה nodes ב Style Tree הם פריטי CSS Selectors כגון "p" או "hover:" ועל כל node יש את סט החוקים שפועלים על selection זה, כגון "border-radius: 4px".

שלב 4: חיבור ה Styles ל DOM ובניית ה Rendering Tree

ה Rendering Tree הוא שלב ביניים בין ה DOM Tree וה styles לציור התוכן בפועל על המסך. העץ מורכב מאלמנטים, שכל אחד מהם מתאר מלבן שמאוחר יותר יצויר על המסך:

אלמנטים ב Render Tree מתייחסים לאלמנטים ב DOM Tree, אך יחס זה הוא לא 1:1. ייתכן מצב בו אלמנט יחיד ב DOM (בדוגמה: אלמנט ה HTML) מיוצג ע"י מספר אלמנטים ב Render Tree. מצד שני, אלמנטים שהם hidden (תגים כמו Script או Head שהם מוחבאים כהגדרה או תגים רגילים שהוחבאו בעזרת הגדרה, כגון תכונת "display:none") פשוט לא יתוארו ע"י ה Render Tree. ה Render Tree מתאר רק את מה שאמור להיות מצויר על המסך.

יוצא דופן היא תכונת ה visibility שאיננה גורמת לכך שלא יווצר Render Tree Node. הסיבה לכך היא שע"פ התקן, בן לאלמנט "visibility:hidden" יכול עדיין להיות בעל "visibility:visible" ולהיות מוצג. כדי לחשב כיצד להציג אותו עדיין צריך ב Render Tree את פרטי האב ה"בלתי נראה".

כאשר רוצים להחיל styles על ה DOM Tree, עוברים node אחר node בתוך ה DOM ומחפשים אלו styles רלוונטיים. את החיפוש מתחילים מתחתית העץ (המקבילה לצד הימני של ה Selector) – מכיוון שכך ניתן לפסול מהר יותר Selectors שאינם רלוונטיים.

CSS Selectors וביצועים

המלצה ידועה למפתחי ווב היא:

  1. "השתמשו כמה שיותר ב ids" – חיפוש שלהם הוא הכי מהיר
  2. "אם אי-אפשר להשתמש ב id, השתמשו ב class" – גם הם מהירים
  3. "אח"כ השתמשו בכל השאר."

המלצה זו היא טובה ככלל אצבע, אך יש עוד לדעת ולעשות על מנת לשפר את ביצועי הדף / אפליקציה שלנו.

ids, למשל, הם מהירים מבחינת ביצועים אך לא כ"כ טובים מבחינת תחזוקת קוד. ישנה דעה רווחת שברגע שהדפדפן מצא את ה id המבוקש – הוא מפסיק לחפש, אולם אם תנסו להכניס 2 ids זהים ב HTML (דבר לא-חוקי בעליל) סביר שתראו שהדפדפן מרנדר את ה style נכון – מה שמצביע שזו לא הדרך שבה עובד הדפדפן פנימית. עובדה זו נכונה אולי לפקודה document.getElementById ועל כן היא קצת יותר מהירה.

נראה שהדפדפנים מחזיקים אינדקסים ל Ids ול classes וכך הם מצליחים להחזיר אותם מהר יותר מאשר לטייל על כל ה DOM.

ההשפעה של Selectors מורכבים

בואו נתאר את מבנה ה DOM ורשימת החוקים הבאים:

הערה: השתמשתי בדוגמה בה ה Selectors מה Style Tree מיוצגים כרשימה, כך שיהיה קל יותר לקרוא אותה.

בואו נבחן את הקושי לדעת אם אלמנט בודד ב DOM Tree מתאים ל Selectors:

  • את 4 ה Selectors הראשונים הכי קל לאשר / לבטל מידית. בחינת ה node הנוכחי מספיקה בכדי להכריע אם החוק תקף או לא. (O(1.
  • חוקים כמו "sidebar p#" דורשים טיול, פוטנציאלי, עד לראש העץ בכדי לדעת אם הם תקפים, כלומר (O(n למרות שיש לנו שני אלמנטים שראינו קודם שכ"א מחושב ב (O(1 כשהוא לבד.
  • ביטוי כמו "ul > p" ניתן לבדוק ב (O(2 – בניגוד לביטוי דומה למדי "ul p" שדורש (O(n.
  • ביטוי מורכב יותר כמו "item div p." ידרוש בתאוריה (O(n^2 מכיוון שעבור כל div בשרשרת האבות צריך להתחיל מחדש חיפוש אחר אב עם class בשם "item", אולם דפדפנים מודרניים יכולים בקלות לבצע אופטימיזציה ל (O(n, מכיוון ששרשרת האבות היא אחת.
  • עוד מקרה מיוחד selector כגון "hover:" שדורש בדיקה כל זמן שהעכבר זז, מה שעלול להפוך selector כגון "someClass div p:hover." למטרד עבור הדפדפן.

ניתוח זה נועד להבהיר את המחיר של הכנסת כל node חדש ל DOM Tree. כאשר מדובר שאילתות על ה DOM Tree מתוך javaScript, המכיר יכול להיות גדול עד פי מספר ה nodes ב DOMTree.
אם אתם עובדים עם jQuery או Zepto בכדי לתשאל את ה DOM בזמן ריצה (כדאי שתעבדו!) – כדאי להכיר כמה טיפים ספציפיים לגבי ספריות אלו.

מה אפשר לעשות בכדי לסייע ל DOM Tree להבנות מהר יותר?

  • כל אלמנט חדש שנכנס ל DOM – נבדק ע"י כל החוקים ב Style Tree. זכרו שדפדפנים קוראים את ה selectors מימין לשמאל ונסו שהאלמנט הימני ביותר ב selections שלכם יהיה ספציפי ככל הניתן, כך שיהיה אפשר לבטל אותו מהר.
  • נסו להימנע מחלקים "ברורים מאליהם" בחוקים שלכם, לדוגמה: אם "sidebar div ul li.item img." זהה בפועל ל "item img." – נסו להשתמש בשני. אני מודה שתיאור מפורט של המבנה לפעמים מסייע לכתוב קוד קריא יותר.
  • נסו להימנע, אם אפשר, מיצירת DOM Tree עמוק במיוחד.
  • נסו להימנע מחוקים מסובכים, נאמר:"{ div:nth-of-type(3) ul:last-child li:nth-of-type(odd) *{ font-weight:bold".ע"י חישוב המצב הספציפי בעצמכם וסימון האלמנטים שעומדים בתנאי ב class – אתם יכולים לחסוך לדפדפן הרבה מאוד עבודה בעת הכנסת אלמנטים ל DOM Tree.

עוד כלל חשוב הוא לטעון את כל קבצי ה CSS מוקדם ככל האפשר, ולפני קבצי ה JavaScript בפרט. מדוע?

  • הוספה של אלמנט ב DOM (נאמר בעקבות קוד ג'אווהסקריפט) גורמת לבנייה של node חדש ב Render Tree, מה שדורש מעבר על כל ה Style Tree, עם האופטימיזציות שהוזכרו לעיל. נציין עבודה זו כ w.
  • הוספה של חוק ל Style Tree דורשת לעבור על כל האלמנטים ב DOM Tree ולבדוק כל אחד מהם את החוק החדש. אם החוק תקף, יש לבנות מחדש את ה Render Tree nodes של כל הבנים של האלמנט עליו תקף החוק החדש – בגלל שתכונות ב CSS הן נורשות. רק תוספת אחרונה זו היא עבודה של m * w (כאשר m הוא מספר ה nodes שעליהם תקף החוק והבנים שלהם) – הרבה יותר מהכנסה של אלמנט ל DOM Tree.
על כן נעדיף תמיד "לסגור" את ה Render Tree שלנו לפני שאנו "נוגעים" ב DOM.

לדפדפנים יש גם עוד מנגנון שניתן לתאר כ DOM Tree write Buffer (עליו נדבר בפוסט ההמשך) שהופך סדרה של כתיבות ל DOM לזולות יותר.

שלב 5: Layout / Reflow

אלמנטים ב Render Tree יכללו הן את תכונות ה CSS Box Model: גובה, רוחב, עובי מסגרת, ריווח וכו' והן תכונות מיקום (position, float, left, top וכו').

What is Box model in CSS? | Lil Engine

גם ערכי תכונות ה box model וגם ערכי תכונות המיקום מושפעות מאלמנטים אחרים ב Rendering Tree. בתהליך שנקרא Layout או Reflow עובר מנוע הרינדור על כל האלמנטים ב Render Tree ומחשב את הרוחב והמיקום שלהם. תכונות כגון float:right (תכונת מיקום) או height:auto (תכונות גודל) משפיעות רבות על ה Layout שייווצר בסוף.
בתהליך בניית ה Render Tree מבוצע חישוב של הרוחבים של האלמנטים.
רוב האלמנטים ב Render Tree יהיו אחד מ – 2 סוגים:

  • Inline: אלמנט שתופס את הרוחב והגובה שלו, אך מאפשר להמשיך את ה inline Flow. כלומר: אם יש כמה אלמנטי Inline אחד אחר השני, הם יסתדרו בשורה כל עוד רוחב העמוד מאפשר זאת (ואז יגלשו לשורה הבאה).
  • Block: אלמנטים הדורשים שורה משלהם.
מקור (הכולל מידע נוסף): https://www.webdesignerdepot.com/2012/09/when-pages-are-not-paper-the-designers-guide-to-layout-code/

על סוגים נוספים, פחות נפוצים, של התנהגות layout ניתן לקרוא כאן.

שלב 6: Canvas & Paint

השלב האחרון של הדפדפן הוא לקחת את ה Render Tree כסדרה של הגדרות ולצייר אותו בפועל על ה canvas של הדפדפן. שלב זה נעשה בעזרת APIs של מערכת ההפעלה ושימוש ב GPU (האצת חומרה) ,אם ניתן – עבור פעולות רינדור פעולות מורכבות כגון Animate או Transform של CSS.

סיכום

בפוסט זה סקרנו את מבנה הדפדפן וה flow החשוב ביותר שלו "הפיכת HTML למסך מרונדר".
בכדי לפשט את הדיון התעלמנו מאלמנט חשוב: האופי הדינמי של הדף הנובע מקוד javaScript.
אנסה בפוסט המשך להוסיף מורכבות נוספת זו להשלמת התמונה.

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

[א] האצת חומרה (hardware acceleration) היא הפעלת פקודות גרפיות, בעיקר, שהמעבד הגרפי (GPU: Graphical Processing Unit), יחידת עיבוד חיצונית ל CPU שיש כיום כמעט לכל מחשב – יכולה לבצע יותר מהר מה CPU. שימוש ב GPU יכול לשפר משמעותית את ביצועי הרינדור של כל מיני פילטרים מתקדמים של CSS3 כגון Transform או Animations, עבודה עם Canvas או תלת-מימד.

מקורות מעניינים נוספים בנושא:

Browsers architecture
How browsers works
כתבה קלאסית ומקיפה של טלי גרסיאל על עבודתם של דפדפנים. פול אייריש (גורו ווב שעובד בגוגל) אהב את הכתבה ופרסם אותה מחדש באתר הפופולרי HTML5 Rocks!
http://www.html5rocks.com/en/tutorials/internals/howbrowserswork/
מעניין לציין שטלי ואני עבדנו במשך תקופה ״בערך״ באותה החברה (אינקפסולה ואימפרבה), אך לא הזדמן לי להכיר אותה.

(How Webkit works (ppt
קצת פרטים ספציפיים על Webkit
https://docs.google.com/presentation/pub?id=1ZRIQbUKw9Tf077odCh66OrrwRIVNLvI_nhLm2Gi__F0#slide=id.p

About Layers
תיאור מפורט כיצד Webkit מבצע את הרינדור ל Canvas – נושא שדילגתי עליו בפוסט זה.
http://www.html5rocks.com/en/tutorials/speed/layers/

AMD ו Require.js

תבנית העיצוב AMD (ראשי תיבות של Async Module Definition) היא בהחלט לא MVC. מדוע עם כן אני עוסק בה בסדרה על MVC?

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

פוסט זה שייך לסדרה: MVC בצד הלקוח ובכלל.

הקדמה

בשנת 2009, בחור בשם קוין דנגור (Kevin Dangoor) יזם פרויקט בשם ServerJS. מטרת הפרויקט: להתאים את שפת ג'אווהסקריפט לפיתוח צד-השרת.

הפרויקט, כלל קבוצות עבודה שהגדירו APIs לצורת עבודה שתהיה נוחה ואפקטיבית בפיתוח ג'אווהסקריפט בשרת.
פרויקט ServerJS הצליח לעורר הדים והשפיע בצורה משמעותית על עולם הג'אווהסקריפט. פרויקטים מפורסמים שהושפעו ממנו כוללים את CouchDB, Node.js ו MongoDB.

ServerJS הצליח כל-כך, עד שגם הדפדפנים (שלהם הפרויקט לא-יועד) החלו לממש רעיונות מתוך ServerJS, בהתאמה קלה לעולם הדפדפנים. אנשי ServerJS קיבלו את האורחים החדשים, ושינו את שם הפרויקט ל: "CommonJS". כלומר: הפרויקט של כ-ו-ל-ם.

עם אלו בעיות ServerJS מנסה להתמודד?

  • הגדרה של מודולים (modules) וחבילות (packages) בכדי לארגן את הקוד. חבילות הן קבוצות של מודולים.
  • כתיבת בדיקות-יחידה.
  • כלי עזר לכתיבת קוד אסינכרוני כך שהקוד יישאר מודולרי  – אותו כיסיתי בפוסט מקביליות עם jQuery (ובכלל) (תקן ה Promises/A, היחסית-מפורסם)
  • עבודה עם מערכות קבצים.
  • טעינה דינמית של קוד.
  • ועוד כמה…
אחד התקנים בעל ההשפעה הרבה ביותר הוא תקן בשם Modules/1.1, תקן המתאר כיצד להגדיר מודולים. צורך זה הוא בסיסי מאוד והרבה frameworks משתמשים ב "CommonJS Sytle" (כלומר – בתחביר של התקן, או כזו שדומה לו מאוד) על מנת להגדיר מודולים של קוד ג'אווהסקריפט.
אנשי הדפדפנים, התאימו את Modules/1.1 לעולם הדפדפן (ישנם כמה הבדלי התנהגות חשובים) וקראו לו: Async Module Definition, או בקיצור: AMD [א].
רק להסביר: AMD היא הגדרה המתארת API לטעינה דינמית של מודולים – אך אין מאחורי AMD קוד. ל AMD יש מימושים רבים בדמות ספריות כמו: lsjs, curl, require, dojo ועוד.
המימוש הבולט ביותר ל AMD היא ספרייה בשם require.js.
כיום require.js היא הספרייה הנפוצה ביותר, בפער גדול, על שאר האלטרנטיבות. המצב מזכיר במעט את המצב של jQuery מול MooTools או Prototype – תקן "דה-פאקטו".

היתרונות של AMD (בעצם: require.js)

מלבד היכולת להפציץ חברים לעבודה במושגים (כמו CommonJS, AMD או Modules/1.1), תבנית-העיצוב AMD מספקת יתרונות משמעותיים לאפליקציות גדולות. מרגע זה ואילך אתייחס ספציפית ל require.js, או בקיצור: "require".

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

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

הערה קטנה: require לא נבנתה לנהל קשרים בהם יש cycles, אולם יש "טכניקה" בה ניתן לטעון קבצים עם תלות מעגלית – אם כי בצורה מעט מסורבלת.

#2: טעינה עצלה ומקבילית של קבצי javaScript [ביצועים]

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

  • Require לא תטען קובץ עד לרגע שצריך אותו בפועל (lazy loading).
  • Require טוענת קבצים בעזרת תכונת ה async של תגית ה – משהו שכמעט בלתי-אפשרי לנהל באופן ידני בפרוייקט גדול.
טעינה דינמית של קבצים מפגישה 2 כוחות מנוגדים:
מפתחים – שמעוניינים בהרבה קבצים קלים בהם קל לנהל את הקוד.
אנשי production / operations שרוצים שיהיו מינימום roundtrips לשרת.
את הפתרון לדילמה זו מספקת require בדמות ספרייה בשם r.js (כמו "require" שעבר minification -ל "r") שיודעת לדחוס רשימה של קבצים לקובץ אחד גדול, לבצע minification ולטעון דינמית רק קוד שלא נמצא שם. לא צריך באמת לציין את כל הקבצים – מספיק להגדיר את הקדקודים הרצויים של גרף התלויות ו r ימצא את כל התלויות שהן חובה ויארוז אותן.
הפתרון שנוצר הוא פתרון כמעט-אופטימלי בין הצרכים השונים.
#3: ניהול תלויות בין קובצי ה javaScript השונים
שני היתרונות הקודמים הם בהחלט חשובים, אך הפאנץ' ליין נמצא כאן, לטעמי.
כאשר אתם מגדירים תלויות בין מודולים – require תסייע לכם לאכוף את התלויות הללו ולוודא שאינכם "עוקפים" אותן.
משהו שבשפות אחרות היינו מקבלים כמשפט "import" או "include" ולעתים היה נראה כמעמסה בעת כתיבה – מתגלה כחשוב מאוד כשהוא חסר.

הניסיונות שלי לנהל פרוייקטים בעזרת namespaces במרחב הגלובלי (בצורת {} || var myns = myns) נגמרו לבסוף בעשרות תלויות בלתי-רצויות בקוד ש"הזדחלו" מבלי שהרגשנו. ברגע שרצינו להשתמש במודולריות של הקוד, כפי שתכננו – לא יכולנו לעשות זאת ללא refactoring משמעותי.

מה שווה MVC, אם ה"מודל" מפעיל פונקציות שלא היה אמור מתוך ה "View"??
מה שווה חלוקה ל Layers, אם היא לא נאכפת בפועל??Require תסייע לכם לוודא שהקוד אכן מיישם את ה design שתכננתם.

מבוא קצר ל Require.js

חטא נפוץ הוא להציג את require.js כספריה קטנטנה ונטולת-מורכבות. מדוע חטא? מכיוון שהרשת מלאה במדריכי "hello world" ל require, המציגים רק את היכולות הבסיסיות ביותר. אחרי כמה שעות עם עבודה ב require קרוב לוודאי שתזדקקו ליותר – אך ידע זה קשה להשגה. התיעוד הרשמי של require הוא טכני ולא הדרגתי – ממש כמו לקרוא מסמך Specification. מתאים בעיקר למי שכבר מתמצא.
התוצאה: עקומת למידה לא קלה לשימוש ב require – וללא סיבה מוצדקת.
require היא בעצם ספריה בוגרת ומקיפה, מלאה באופציות אפשריות לשימוש. הממשק הפשוט שלה מעט מטעה: הוא נועד לספק קלות פיתוח – אך הוא מסתיר מורכבות רבה שקל להתבלבל בה.
אני אתחיל בחטא, אך אתקן אותו בהמשך.

מבוא נאיבי ל Require.js

Require היא ספריה פשוטה וחמודה.היא מציגה בסה"כ 3 פקודות:

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

ModuleID הוא מזהה טקסטואלי שם המודול. ה Id בעזרתו אוכל לבקש אותו מאוחר יותר.
את הקוד של המודול כותבים בתוך פונקציה, כך שלא "תלכלך" את המרחב הגלובלי (global space).
קונבנציה מקובלת ומומלצת היא לחשוף את החלק הפומבי (public) של המודול בעזרת החזרת object literal עם מצביעים (ורוד) לפונקציות שאותם ארצה לחשוף (טורקיז). כמובן שאני יכול להגדיר משתנים / פונקציות נוספים שלא ייחשפו ויהיו פרטיים.

מבנה זה נקרא "Revealing Module" והוא פרקטיקה ידועה ומומלצת בשפת javaScript. ספריית require מסייעת להשתמש במבנה זה. דיון מפורט במבנה זה ניתן למצוא תחת הפסקה "הרצון באובייקטים+הכמסה = Module" בפוסט מבוא מואץ ל JavaScript עבור מפתחי Java / #C מנוסים – חלק 2.

נניח שיש לי קוד שזקוק למודולים 1 ו 2 בכדי לפעול, כיצד הוא מתאר תלות זו וגורם להם להטען?
פשוט מאוד:

הפונקציה בדוגמה היא callback שתפעל רק לאחר שהקוד של מודולים 1 ו2 נטען ואותחל. m1 הוא reference ל מודול1 (אותו object literal שהוחזר ב return וחושף את החלקים הציבוריים) ו m1 הוא reference למודול2. השיוך נעשה ע"פ סדר הפרמטרים.

doStuff היא כבר סתם פונקציה שעושה משהו עם m1 ו m2.

בפועל, רוב הפעמים יהיו לנו קוד שגם:

  1. מגדיר מודול.
  2. וגם תלוי במודולים אחרים.

משהו שנראה כך:

קוד זה הוא מעט מסורבל, ויותר גרוע – טומן בתוכו חשיפה לאופי האסינכרוני בו טוענת require את קובצי ה javascript. ייתכן וה return יופעל לפני ש doStuff הוגדרה – מה שיחזיר undefined כמצביע ל doStuff לקוד שביקש אותו.

כתיבת קוד אסינכרוני שתבטיח שה return יופעל רק לאחר ש doStuff הוגדרה תוסיף עוד מספר שורות קוד – ותהפוך את קטע הקוד למסורבל עוד יותר. על כן require (בעצם AMD) הגדירה תחביר מקוצר למצב של מודול שתלוי בקוד אחר. זהו בעצם המצב הנפוץ ביותר:

הנה, קוד זה כבר נראה אלגנטי וקצר. הפונקציה שהגדרנו בשורה הראשונה היא ה callback שיקרא רק לאחר שמודולים 1 ו2 הופעלו – ממש כמו בפקודת require.

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

  • כאשר אנו רוצים לטעון מודולים רק בהסתעפות מסוימת בקוד (ולכן איננו יודעים בוודאות על צורך זה בשורה הראשונה).
  • עבור הקובץ הראשון בתוכנה שלנו. כלומר: פונקציית ה "main".
הדרך המקובלת ביותר לטעון את require ב HTML היא באופן הבא:
שימו לב שגם בפרויקט גדול, אין צורך להגדיר ב HTML יותר מסקריפט אחד: require. הוא כבר יטען את כל השאר.
data-main הוא שם קובץ ה javascript של פונקציית ה "main" שלנו שמאתחלת את התכנית. יש להקליד את שם הקובץ ללא סיומת .js.
זוכרים שיש פקודה שלישית? config? – היא לא כ"כ חשובה.
היא משמשת להגדרות גלובליות מתקדמות לגבי ההתנהגות של require. למשל קטע הקוד הבא:

קוד זה מגדיר שאם קובץ לא נטען (ברשת) תוך 10 שניות, require יוותר ויזרוק exception. מצב זה סביר בעיקר כאשר אתם טוענים קובץ מאתר מרוחק. ה default הוא time-out של 7 שניות.

זהו, סיימנו!

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

סיכום

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

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

—-[א] למען הדיוק אפשר לציין ש AMD התחיל כ Modules/Transport/A (תחת קורת הגג של CommonJS, אם השם המוזר לא הבהיר זאת) – אך הוא נזנח תוך כדי עבודה. כרגע מנסים להחזיר אותו חזרה "הביתה" ל CommonJS בדמות התקן Modules/AsynchronousDefinition, בעיקר על בסיס העבודה שנעשתה ב AMD.

פיתוח למובייל: Native או Web? (חלק ב')

בפוסט הקודם, פיתוח למובייל: Native או Web (חלק א'), סקרנו כוחות המשפיעים על מספר הפלטפורמות עבורן נדרש לפתח אפליקציית מובייל. ריבוי הפלטפורמות הוא גורם מרכזי המעודד פיתוח אפליקציות ווב, אך בשל שיקולי UX והתאמה לחווית המשתמש – נוהגים פעמים רבות לכתוב גם אפליקציות ווב בוריאציות שונות ע"פ גודל המכשיר / מערכת ההפעלה.בחלק זה, אני רוצה להתבונן על האופציות העומדות לרשותנו לפיתוח אפליקציות Multi-Device וה Tradeoff הטכנולוגי בניהן.

Native או Web: הדילמה

"פיתוח Native" הוא המקבילה של Desktop Application בעולם המובייל, קרי אפליקציה המותקנת על המכשיר, רצה כקובץ בינארי ומשתמשת בפקדים של מערכת ההפעלה (בהגדרה גסה).

יתרונות עיקריים של פיתוח Native הם:

  • Best User Experience בפיתוח ב Native קל יותר להגיע לשימושיות גבוהה. הרבה יותר קל להשיג אנימציה חלקה ותגובה מהירה, ולעתים יש דברים שפשוט לא עובדים בווב. מכיוון ששימושיות גבוהה היא ערך חשוב באפליקציות מובייל – זהו יתרון חשוב.חברות המושקעות בפיתוח לווב, מנסות להוכיח ש HTML5 "יכול הכל". סיפור משעשע הוא הסיפור של FastBook בו הציגה חברת Sencha אפליקציית מובייל עדיפה על אפליקציית הנייטיב של פייסבוק (לטענתם), על רקע טענות של צוקרברג על כך ש HTML5 עדיין אינה מספיק טובה.
    Sencha גם עורכת תחרות בשם HTML5isReady ברוח דומה.

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

  • Full Access to OS APIs אמנם ניתן לגשת כיום בחלק מהמכשירים ל GPS או אפילו למצלמה מקוד JavaScript של אפליקציית ווב, אך עדיין יש סט גדול של יכולות הזמינות רק מקוד native על המכשיר: גישה לאנשי הקשר, חיבור לרשתות חברתיות עם פרטי בעל המכשיר, ניהול קבצים ועוד.
  • App Store Distribution. הדרך הסטנדרטית לחפש אפליקציה למובייל היא כמובן דרך ה App Store של המכשיר – הזמינה רק לאפליקציית נייטיב. עבור חברה קטנה שמוכרת אפליקציות – גישה ל App Store היא הכרחית על מנת להפיץ את האפליקציה. עוד בנושא ניתן לקרוא בלינק לפוסט על חנויות אפליקציה.

לפיתוח אפליקציות Web יש כמה יתרונות חשובים משלהן:

  • Easiest Multi-Platform Development קלות בפיתוח למספר מכשירים. שימו לב שאני אומר קלות ולא "פיתוח יחיד". זאת בשל קושי לבצע Full User Experience Reuse ובשל שכיחות הבאגים בדפדפנים מוביילים (בולט במיוחד: Android Browser על גרסאות אנדרואיד 2.3 ומטה).
  • Full control on updates באפליקציות נייטיב אינני יכול לשלוט מתי/האם משתמשים מעדכנים את גרסת האפליקציה – כך שה Backend שלי צריך לתמוך בגרסאות שונות. אם יש לי עדכון קריטי (אולי עדכון אבטחה?) – בווב אשחרר אותו לכל המשתמשים מיידית. בנייטיב: תהליך האישור של חנות האפליקציות יכול לעכב אותי בכשבוע או יותר בשחרור השינוי הקטן ביותר. תכונה זו חשובה יותר לארגונים.
  • Interoperability & Extendibility נקודה זו היא חסרת חשיבות כמעט ל Consumer אך חשובה מאוד ל Enterprises ועושה לעתים רבות את ההבדל: מכיוון שאפליקציות נייטיב רצות ב Sandbox – אין מודל קיים של Plug-in שארגון יכול להשתמש בו על מנת לבצע התאמות של אפליקציית נייטיב לצרכיו הספציפיים. מאידך גיסא הרחבות בווב (ב JavaScript) הן קלות מתמיד – ויש הרבה ידע ותשתיות קיימות. אלמנט נוסף שחשוב למחלקות ה IT הוא Branding: היכולת לצבוע את האפליקציה בצבעים והלוגו של החברה (כדי שיהיה ברור מעורבותה של מחלקת ה IT). לקוחות רבים דחו אפליקציות בגלל יכולת זו בלבד, סיבה שנראית כחסרת-חשיבות למי שלא מבין את הפוליטיקה הפנימית של ארגוני IT בארגונים גדולים.

פתרון הביניים: Hybrid

הפלא ופלא, אנשים יצירתיים חשבו על פתרון לשלב בין גישות ה Web וה Native במכשירי המובייל. בכלי הפתוח למערכות ההפעלה השונות ישנו פקד (פקד = control. ממש כמו כפתור או רשימה) שנקרא WebView ומהותו הוא הצגת תוכן וובי בתוך אפליקציית native.

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

  • בתור התחלה: ניתן להפיץ את האפליקציה ב App Store, מכיוון שמעתה זו אפליקציית נייטיב לכל דבר.
  • ניתן לשמור את קבצי ה HTML מקומית מבלי "לחלוק" Cache עם אפליקציות אחרות ולהבטיח טעינה מהירה יותר של האפליקציה.
  • ניתן, ופה אולי היתרון יוצא-הדופן, לחשוף פונקציות של האפליקציה הנייטיב כ javaScript API כך שאפליקציית הווב תוכל להשתמש בהן – וכל להעשיר את אפליקציית הווב ביכולות ״נייטיב״. לדוגמה: חישובים שדורשים יעילות גבוהה (כמו פענוח הצפנה), גישה ל OS API כגון שליפת אנשי הקשר, הפעלת רטט, או גישה לתמונות שצולמו לאחרונה בעזרת המכשיר.

ישנם פתרונות סטנדרטיים, לדוגמה PhoneGap או MoSync, של Containers מוכנים-מראש לאפליקציות ווב הנקראים Hybrid Container או Hybrid Web Container. ע"י שימוש בפתרונות אלו, אתם יכולים להתמקד בפיתוח אפליקציות ווב (עם התכונות הנלוות של הווב) ורק "לארוז אותן" כאפליקציית נייטיב. PhoneGap מספקת Containers מותאמים למספר רב של מערכות הפעלה מוביליות, וחושפת JavaScript API אחיד ממערכות ההפעלה כך שתוכלו לכתוב אפליקציית ווב פעם אחת, ולגשת לשירותים של מערכת ההפעלה השונות מבלי להיות מודעים למערכת ההפעלה שמריצה את ה Container.

רשימת הנייטיב API ש PhoneGap חושפת עבור אפליקציות ווב, על פלטפורמות שונות. מקור: וויקיפדיה. (לחצו על התמונה להגדלה).

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

פיתוח בעזרת Framework הכולל Hybrid Container מספק יתרונות משני העולמות, הווב והנייטיב:

  • גישה מלאה ל API של מערכת ההפעלה. אם זה לא API שמסופק Out of the Box ע"פ ה Framework, אני יכול להוסיף wrapper של קוד נייטיב החושף javaScript API בעצמי.
  • הפצה של האפליקציה ב App Store.
  • קלות בפיתוח Cross Platform (באופן חלקי: ייתכן ועלי לתחזק wrappers משלי)
  • שליטה מלאה על עדכונים (חלקי: עבור חלק הקוד הוובי באפליקציה. עבור שינויים בקוד הנייטיב עלי לעדכן גרסה של ה container / native app).
  • Interoperability / Extendibility (חלקי: רק עבור חלקי הקוד הוובי).
  • ה User Experience יהיה עדיין נתון רובו ככולו ליכולות הווב, קרי HTML5/CSS3.

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

  • את ההבדל מעל מערכות הפעלה שונות קשה להחביא, ו"ה javaScript API האחיד" מעל מערכות ההפעלה השונות לא מתנהג כ"כ יפה.
  • ישנו פער זמנים בין הזמן בו משתחררת גרסת מערכת הפעלה (למשל iOS 6) לזמן בו משתחררת גרסת ה Container שתומכת בה. מדובר בחודשים. במקרי הקצה, פונקציונליות יכולה להישבר (תמיכה לאחור איננה הצד החזק של אפל, למשל) ולדרוש מכם לכתוב wrappers משלכם, ללא תלות ב Frameworks.
  • ה Hybrid Frameworks מוסיפים Layer נוסף של תלות גם ברמת הבאגים והבעיות השונות. שוב סיבה לעקוף את ה Framework בכדי ליצור workarounds. איתור תקלה (האם היא באפליקציה שלכם, ב Hybrid Container או במערכת ההפעלה?) הופכת למשימה קצת יותר מורכבת. באג רציני דורש הבנה ב Internals של מערכת ההפעלה – כך שהניתוק ממערכת ההפעלה אינו מוחלט.
  • עליכם להקים ולתחזק סביבת Build לכל הפלטפורמות שאתן רוצים לתמוך בהן, משימה שאיננה פשוטה למפתח הבודד. את ה custom native code שאתם מפתחים עליכם לתחזק לכל פלטפורמה (ואולי אף גרסאות שונות של מערכת ההפעלה) – כך שיש אלמנט של קוד כפול.

אינני רוצה ליצור את התחושה ש Hybrid Framework הוא פתרון לא-מוצלח. לפתרון Hybrid יש הרבה ערך ופוטנציאל והם יכולים להיות בסיס לפתרונות מצויינים – בעיקר כשמבינים את ה Tradeoffs שבאים איתם. הדבר העקרי לו כדאי להיות מודעים הוא פער גדול בין ציפיות גבוהות למדי ומציאות שאיננה כולה דבש. אחוז לא מבוטל של מפתחים מתחיל לעבוד עם Hybrid Container בציפייה ל Silver Bullet רק כדי לגלות שיש גם מורכבויות – ואז נוטש אותו.

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

Native Transformers

קטגוריה נוספת ואחרונה של כלי פיתוח מובייל עבור Multi-Device נקראת Native Transformers. נניח שהחלטתם ששימושיות וגישה למערכת ההפעלה חשובה מספיק עבורכם, והתחלתם לפתח נייטיב 6 גרסאות של האפליקציה עבור 3 מערכות הפעלה שונות (+ טאבלט / סמארטפון).

עבור iOS עליכם לפתח בשפת Objective-C ב XCode על גבי מחשב מק.
עבור Android אתם מפתחים בג'אווה על איזו סביבה שמתחשק לכם, וב Eclipse.
עבור Windows Phone / 8 אתם מפתחים בשפת #C על גבי VS2012 שרץ רק על גבי Windows 8.

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

כתבו הרבה קוד משותף, ומעט קוד ספציפי – ההבטחה של ה Native Transformers.
Native Transformers מבטיחים לעזור בנושא זה. הם מציעים לכם לכתוב פעם אחת את הקוד שיכול להיות משותף, בשפת ביניים (לרוב javaScript או וריאציות שלה), בעוד ה Native Transformer יקמפל קוד זה לקוד נייטב עבור מערכות ההפעלה השונות, וייצור לכם שלד של פרויקט בסביבה המתאימה ממנו תוכלו להמשיך. יתרה מכך, חלק מהפתרונות בנויים כך שתוכלו לבצע שינויים בקוד המשותף ולהעביר את התוצר לסביבות הפיתוח הספציפית (לדוגמה: iOS, Windows 8) מבלי למחוק / לפגוע בפיתוחים הספציפיים שכבר עשיתם. מבנה שדומה להורשה בו רק מחלקת האב משתנה ברגע שמקמפלים את הקוד המשותף.

מכיוון שמספר גדול של Native Transformers מתבסס על javaScript כשפת ביניים, לא נדיר למצוא פתרונות שיכולים גם "לקפמל" גרסאות HTML5, כלומר ווב.
תכונה נוספת מקובלת היא Designer מסוג WYSIWYG בו אתם יכולים להרכיב בשפת הביניים את ה UI הכללי ולקבל שלד מוכן של UI בפקדים של מערכת ההפעלה הספציפית.

בואו נשווה את ה Native Transformers לפתרונות שקרובים אליו ביותר, הרי הם פיתוח נייטיב על הרבה פלטפורמות מצד אחד, ופתרונות ה Hybrid מהצד השני:

ישנו מספר רב למדי (אני שמעתי על כ 10 ויותר) של Native Transformers, בעיקר בעולם ה Enterprise. השלושה הנפוצים ביותר הם כנראה:

  • Appcellerator Titanium – הפלטפורמה המוכרת ביותר בקטגוריה זו.
  • Adobe Air – בעיקר עבור תוכנות גרפיות / משחקים. מספר לא מבוטל של משחקים שאתם משחקים בסאמרטפון שלכם ונראים נייטיב לחלוטין נכתבו בעצם ב Air.
  • Antenna – שחקן מרכזי בשוק האנטרפרייז.

כפי שציינתי קודם, כלי Native Transformers רבים משחקים בשוק האנטרפרייז ומציעים יכולות חיבור קלות למערכות עסקיות (למשל SAP או IBM), ספריות אבטחה מתקדמות או ספריות המסייעות לעמוד ברגולציות שונות.

ל Native Transformers יש כמה חסרונות:

  • בדומה לפתרונות Hybrid – יש כאן תלות בשחקן צד שלישי, בקצב העדכונים שלו, האיכות שלו, היציבות הפיננסית שלו וכו'. התלות בספק ה Native Transformers גדולה משמעותית מהתלות בספק ה Hybrid Container, אותו קל יחסית "לעקוף".
  • שפת הביניים, הספריות שלה ובעיקר כלי העיצוב הגרפי הם סביבה חדשה (ולא כ"כ סטנדרטית) שיש ללמוד – מה שהופך את ה Learning curver לגבוה, למרות ש Native Containers ממוצבים ככלי Productivity לפיתוח מהיר על מספר פלטפורמות. בכל מקרה עליכם לדעת לפתח נייטיב בכל הפלטפורמות ולהכיר מספיק טוב את מערכות ההפעלה – הרי ה Native Transformers יוצרים בסה"כ שלד דיי גס של אפליקציה שיש עוד לעבוד עליו.
  • גודל ה binaries של האפליקציה גדל בצורה משמעותית. פי 10 הוא לא מספר חריג במיוחד. זה לא נשמע מעניין כ"כ בימנו, שהרשתות מאוד מהירות, אולם מערכות הפעלה מוביליות לא מרשות (משיקולי שמירה על סוללה) להוריד ב 3G אפליקציות מעל גודל מסוים (בימנו הגבול הוא 20-50MB, תלוי במערכת / גרסה). כלומר: ייתכן והאפליקציה שתיווצר, גם אם היא נראית פשוטה – לא תוכל להיות מותקנת ללא חיבור WiFi.
  • פתרונות אלו הם לרוב מסחריים, ובעלי רישיונות יקרים מהממוצע.

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

הרבה אופציות. כיצד בוחרים?

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

לו הדילמה הייתה פשוטה, קרוב לוודאי שהפוסט לא היה מתארך כ"כ J.
סה"כ הקו המנחה הוא כזה: ארבעת הקטגוריות נמצאות על סקאלה: בצד אחד פתרונות ה Web (הכי זול לפתח Cross-Device, אבל חווית המשתמש הכי פחות אינטגרטיבית) ובצד השני האופציה לפתח נייטיב בנפרד על כל פלטפורמה.
 
עליכם לבחון את המוצר שלכם ולראות היכן הוא הכי מתאים. מה יותר חשוב לכם: עלות-פיתוח נמוכה או חווית שימוש עליונה?
אם אתם סטארט-אפ קטן, עובדים Lean או רק מתחילים – כנראה שעדיף לכם ללכת בגישה של Web-First (אולי רק עם Container לצורך הפצה – אם מדובר באפליקציה לצרכן הפרטי).
אם אתם עסק שמגלגל מיליונים, והאפליקציה היא ה Main business שלכם (למשל פייסבוק) – שווה לפתח נייטיב. לחברה גדולה שזה הביזנס שלה אין מה לנסות ולחסוך צוות או שניים של מפתחים.
 
שיקול נוסף הוא הארגון מול הצרכן הפרטי: למרות תהליכי ה Consumerization בארגון המדוברים כל-כך היום, הצרכן הארגוני הוא עדיין סלחן יותר לחווית המשתמש: יש לו הרבה מוטיבציה לעשות את העבודה. בנוסף, לשיקולי אבטחה (עדכונים מהירים) ויכולת ההתאמה האישית לארגון – יש משקל רב אצל הלקוח הארגוני.
לא מזמן נכתבו אצלנו מספר אפליקציות נייטיב מ Scratch בטכנולוגיות ווב, מכיוון שלקוחות לא הסכימו לרכוש אותן ללא יכולות customization שאינן אפשריות בנייטיב. הכוונה הייתה להשקיע בלקוחות ולייצר חווית שימוש נהדרת ומרשימה – אבל הם הבהירו מה סדרי העדיפויות שלהם.

לסיכום

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

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

דיסקליימר: החברה בה אני עובד, SAP, מוכרת גם כלי Native Transformers. לא התנסיתי בהם, אבל אני מניח שהם הטובים בקטגוריה.

—-

[א] אני קצת מוטה לעולם הארגוני, כנראה. אפליקציות ל Consumer הן רובן נייטיב עדיין.

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

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

בפוסט הקודם, ביצועים של אפליקציות ווב: מבוא לצד האפל, התבוננו בהשפעה המשמעותית של הרשת על ביצועי אפליקציות הווב. ראינו ש ל-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 – שיפור גדול מאוד.

סיכום

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