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

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

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

מדידה

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

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

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

עכשיו נראה כמה האפליקציה הזו גדולה:

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

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

הצלחנו להפחית את גודל החבילה הזו בהרבה במסגרת הקודלאב הסרת קוד שלא בשימוש, אבל 225KB עדיין גדול מאוד.

הקטנה

נבחן את מקטע הקוד הבא.

function soNice() {
  let counter = 0;

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

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

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

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

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

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

גודל הקובץ מגיע עכשיו ל-62 בייטים.

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

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

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

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

כדי לקבל מושג איך נראה הקוד הממוזער, לוחצים על main.bundle.js בחלונית Network ב-DevTools. עכשיו לוחצים על הכרטיסייה Response.

תגובה מקוצרת

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

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

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

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

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

חשוב לבטל את השינויים כאן לפני שממשיכים.

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

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

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

דחיסה

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

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

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

  • General מייצג כותרות כלליות שרלוונטיות לכל האינטראקציה של הבקשה והתגובה.
  • בכותרת התגובה מוצגת רשימה של כותרות שספציפיות לתגובה עצמה מהשרת.
  • בקטע כותרות בקשה מוצגת רשימה של כותרות שהלקוח צירף לבקשה.

כדאי לעיין בכותרת accept-encoding ב-Request Headers.

כותרת Accept encoding

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

  • Gzip (gzip): פורמט הדחיסה הנפוץ ביותר לאינטראקציות בין שרת ללקוח. הוא מבוסס על האלגוריתם Deflate, ויש תמיכה בו בכל הדפדפנים הנוכחיים.
  • 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 ושימוש ב-middlware של 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');

מוסיפים אותו כתוכנה לעיבוד נתונים (middleware) לפני הרכבת express.static:

//...

const app = express();

app.use(compression());

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

//...

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

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

מ-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

הקבצים הסופיים בתוצאה בספרייה ציבורית

הגרסה הזו של החבילה, main.bundle.js.gz, שמקומפרסת באמצעות GZIP, נשמרת כאן עכשיו גם כן. 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() מוודא שהרצף ממשיך לכל קריאה חוזרת (callback) שתתבצע לאחר מכן.

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

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

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

סיכום

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