-->

יום שני, 28 בינואר 2013

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

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

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

http://facebook.com/like-button-script.js

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

http://mysite.com/like-button-script.js

נוצרת בעיה: ה 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 שניות. לא טוב - אבל כנראה סביר.
[ב] סיפור מעניין הוא שאלגוריתם זה כשל ברשתות אלחוטיות / סללוריות בהן אחוז הכשלים ה"טבעי" הוא גבוה יחסית. היה צורך "להתאים" את האלגוריתם לרשתות אלו על מנת שיעבדו בצורה נכונה.


7 תגובות:

  1. ישר כוח ליאור! כתבה מצויינת, כרגיל

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

    השבמחק
  3. פוסט מצויין, תודה!

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

    השבמחק
    תשובות
    1. נקודה טובה!, תודה.

      הנה פוסט שדן בנושא לעומק:
      http://goo.gl/EqdpcE

      ליאור

      מחק