הקטנה ודחיסה של מטענים ייעודיים (payload) ברשת באמצעות gzip

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

צילום מסך של אפליקציה

מדידה

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

  • כדי לראות תצוגה מקדימה של האתר, לוחצים על View App (הצגת האפליקציה) ואז על Fullscreen מסך מלא (מסך מלא).

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

עכשיו, נבחן את גודל האפליקציה:

  1. מקישים על 'Control+Shift+J' (או על 'Command+Option+J' ב-Mac) כדי לפתוח את כלי הפיתוח.
  2. לוחצים על הכרטיסייה Network.
  3. מסמנים את התיבה Disable cache (השבתת מטמון).
  4. טוענים מחדש את האפליקציה.

גודל החבילה המקורי בחלונית 'רשת'

למרות שהושגה התקדמות רבה ב-codelab "Remove unused code" (הסרת קוד שאינו בשימוש) כדי להקטין את גודל החבילה, 225KB עדיין גדול למדי.

הקטנה

נבחן את בלוק הקוד הבא.

function soNice() {
  let counter = 0;

  while (counter < 100) {
    console.log('nice');
    counter++;
  }
}

אם הפונקציה הזו שמורה בקובץ משל עצמה, גודל הקובץ הוא בערך 112B (בייטים).

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

function soNice(){let counter=0;while(counter<100){console.log("nice");counter++;}}

גודל הקובץ עכשיו יהיה כ-83B. אם הוא יעובד עוד יותר על ידי צמצום אורך שם המשתנה ושינוי של ביטויים מסוימים, הקוד הסופי עשוי להיראות כך:

function soNice(){for(let i=0;i<100;)console.log("nice"),i++}

גודל הקובץ מגיע עכשיו ל-B62.

בכל שלב קשה יותר לקרוא את הקוד. עם זאת, מנוע ה-JavaScript של הדפדפן מפרש כל אחד מהם בדיוק באותו אופן. היתרון של ערפול קוד באופן הזה יכול לעזור ליצור קבצים בגדלים קטנים יותר. 112 B למעשה לא הייתה הרבה בהתחלה, אבל עדיין הייתה ירידה של 50% בגודל!

באפליקציה הזו, webpack גרסה 4 משמש כחבילה של מודולים. ניתן לראות את הגרסה הספציפית ב-package.json.

"devDependencies": {
  //...
  "webpack": "^4.16.4",
  //...
}

גרסה 4 כבר מקטינה את החבילה כברירת מחדל במהלך מצב הייצור. היא משתמשת בפלאגין TerserWebpackPlugin ל-Terser. Terser הוא כלי פופולרי לדחיסת קוד JavaScript.

כדי להבין איך הקוד המוקטן נראה, אפשר ללחוץ על main.bundle.js מתוך החלונית רשת של כלי הפיתוח. עכשיו לוחצים על הכרטיסייה Response.

תגובה מוקטנת

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

module.exports = {
  mode: 'production',
  mode: 'none',
  //...

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

גודל החבילה: 767KB

זה הבדל גדול למדי! 😅

לפני שממשיכים, צריך לבטל את השינויים כאן.

module.exports = {
  mode: 'production',
  mode: 'none',
  //...

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

  • אם משתמשים ב-webpack מגרסה 4 ואילך, לא צריך לבצע פעולות נוספות כי הקוד מוקטן כברירת מחדל במצב ייצור. 👍
  • אם משתמשים בגרסה ישנה יותר של Webpack, מתקינים את TerserWebpackPlugin וכוללים את TerserWebpackPlugin בתהליך ה-build של ה-webpack. במסמכי התיעוד יש הסבר מפורט.
  • קיימים גם יישומי פלאגין אחרים להקטנה, ואפשר להשתמש בהם במקומם, כמו BabelMinifyWebpackPlugin ו-ClosureCompilerPlugin.
  • אם לא משתמשים בכלל ב-Module Bundle, צריך להשתמש ב-Terser ככלי CLI או לכלול אותו ישירות כתלות.

דחיסה

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

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

בכל בקשה ותגובה של HTTP, דפדפנים ושרתי אינטרנט יכולים להוסיף headers כך שיכללו מידע נוסף על הנכס שמאוחזר או מתקבל. אפשר לראות את זה בכרטיסייה Headers בחלונית של רשת כלי הפיתוח, שבה מוצגים שלושה סוגים:

  • General מייצג כותרות כלליות שרלוונטיות לכל האינטראקציה מסוג בקשה-תגובה.
  • בכותרת התגובה מוצגת רשימה של כותרות שספציפיות לתגובה עצמה מהשרת.
  • Request Headers (כותרות של בקשות) מציג רשימה של כותרות שמצורפות לבקשה על ידי הלקוח.

כדאי לקרוא את הכותרת accept-encoding ב-Request Headers.

אישור כותרת הקידוד

הדפדפן משתמש ב-accept-encoding כדי לציין באילו פורמטים של תוכן הוא מקודד תוכן או אילו אלגוריתמים לדחיסה הוא תומך בהם. יש הרבה אלגוריתמים לדחיסת טקסט, אבל יש כאן רק שלושה מהם לצורך הדחיסה (וביטול הדחיסה) של בקשות רשת מסוג HTTP:

  • Gzip (gzip): פורמט הדחיסה הנפוץ ביותר לאינטראקציות בין שרתים ולקוח. הוא מבוסס על האלגוריתם של Deflate ונתמך בכל הדפדפנים הקיימים.
  • ערעור (deflate): לא בשימוש נפוץ.
  • Brotli (br): אלגוריתם דחיסה חדש יותר שמטרתו לשפר עוד יותר את יחסי הדחיסה, שיכולים להוביל לטעינה עוד יותר מהירה של דפים. היא נתמכת בגרסאות האחרונות של רוב הדפדפנים.

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

דחיסה דינמית

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

יתרונות

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

חסרונות

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

דחיסה דינמית באמצעות Node/Express

הקובץ server.js אחראי להגדרת שרת ה-Node שמארח את האפליקציה.

const express = require('express');

const app = express();

app.use(express.static('public'));

const listener = app.listen(process.env.PORT, function() {
  console.log('Your app is listening on port ' + listener.address().port);
});

כרגע אפשר לייבא את express ולהשתמש בתווכה express.static כדי לטעון את כל קובצי ה-HTML הסטטיים, ה-JS וה-CSS בספרייה public/ (והקבצים האלה נוצרים על ידי Webpack בכל build).

כדי לוודא שכל הנכסים יידחסו בכל פעם שנשלחת בקשה, אפשר להשתמש בספריית תווכה לדחיסה. כדי להתחיל, צריך להוסיף אותו בתור devDependency ב-package.json:

"devDependencies": {
  //...
  "compression": "^1.7.3"
},

ומייבאים אותו לקובץ השרת, server.js:

const express = require('express');
const compression = require('compression');

ומוסיפים אותה כתווכה לפני הטעינה של express.static:

//...

const app = express();

app.use(compression());

app.use(express.static('public'));

//...

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

גודל החבילה עם דחיסה דינמית

מ-225KB עד 61.6KB! ב-Response Headers עכשיו, הכותרת content-encoding מראה שהשרת שולח את הקובץ הזה עם הקידוד של gzip.

כותרת קידוד תוכן

דחיסה סטטית

הרעיון שעומד מאחורי דחיסה סטטית הוא נכסים דחוסים ונשמרים מראש.

יתרונות

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

חסרונות

  • צריך לדחוס את הנכסים בכל גרסת build. זמני ה-build יכולים להתארך באופן משמעותי אם משתמשים ברמות דחיסה גבוהות.

דחיסה סטטית באמצעות Node/Express ו-webpack

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

כדי להתחיל, צריך להוסיף אותו בתור devDependency ב-package.json:

"devDependencies": {
  //...
  "compression-webpack-plugin": "^1.1.11"
},

כמו כל פלאגין אחר של Webpack, מייבאים אותו בקובץ התצורות, webpack.config.js:

const path = require("path");

//...

const CompressionPlugin = require("compression-webpack-plugin");

ונכלול אותו במערך plugins:

module.exports = {
  //...
  plugins: [
    //...
    new CompressionPlugin()
  ]
}

כברירת מחדל, הפלאגין דוחס את קובצי ה-build באמצעות gzip. כדאי לקרוא את מסמכי התיעוד כדי ללמוד איך להוסיף אפשרויות לשימוש באלגוריתם אחר או איך לכלול או להחריג קבצים מסוימים.

כשהאפליקציה נטענת מחדש ונבנית מחדש, נוצרת עכשיו גרסה דחוסה של החבילה הראשית. פותחים את Glitch Console כדי לראות מה נמצא בספרייה הסופית public/ שמוצגת על ידי שרת ה-Node.

  • לוחצים על הלחצן כלים.
  • לוחצים על הלחצן Console.
  • במסוף, מריצים את הפקודות הבאות כדי לבצע שינויים בספרייה public ולראות את כל הקבצים שבה:
cd public
ls

הקבצים שהפלט האחרון התקבל בספרייה הציבורית

גרסת gzip של החבילה, main.bundle.js.gz, שמורה עכשיו גם כאן. CompressionPlugin גם מכווצת את index.html כברירת מחדל.

הדבר הבא שצריך לעשות הוא להנחות את השרת לשלוח את קובצי ה-gzip האלה בכל פעם שנשלחה בקשה לגרסאות ה-JS המקוריות שלהם. אפשר לעשות זאת על ידי הגדרת מסלול חדש ב-server.js לפני שהקבצים מוצגים עם express.static.

const express = require('express');
const app = express();

app.get('*.js', (req, res, next) => {
  req.url = req.url + '.gz';
  res.set('Content-Encoding', 'gzip');
  next();
});

app.use(express.static('public'));

//...

app.get משמש כדי להורות לשרת איך להגיב לבקשת GET לנקודת קצה ספציפית. אחר כך משתמשים בפונקציית קריאה חוזרת כדי להגדיר איך לטפל בבקשה הזו. המסלול פועל כך:

  • אם מציינים את '*.js' כארגומנט הראשון, זה פועל בכל נקודת קצה שמופעלת כדי לאחזר קובץ JS.
  • בקריאה החוזרת, .gz מצורף לכתובת ה-URL של הבקשה, וכותרת התשובה Content-Encoding מוגדרת ל-gzip.
  • לבסוף, next() מבטיח שהרצף ימשיך לכל קריאה חוזרת שיכולה להיות הבאה.

אחרי שהאפליקציה נטענת מחדש, בודקים שוב את החלונית Network.

צמצום גודל החבילה באמצעות דחיסה סטטית

בדיוק כמו קודם, הופחתה ירידה משמעותית בגודל החבילה!

סיכום

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