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

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

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

מדידה

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

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

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

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

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

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

אמנם השגנו התקדמות רבה ב-codelab הסרת קוד שלא בשימוש כדי לצמצם את גודל החבילה, אבל 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 מייצג כותרות כלליות שרלוונטיות לכל האינטראקציה של הבקשה והתגובה.
  • בקטע Response Headers מוצגת רשימה של כותרות ספציפיות לתגובה בפועל מהשרת.
  • בקטע כותרות בקשה מוצגת רשימה של כותרות שהלקוח צירף לבקשה.

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

כותרת Accept encoding

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

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

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

דחיסת נתונים דינמית

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

יתרונות

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

חסרונות

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

דחיסת נתונים דינמית באמצעות 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).

כדי לוודא שכל הנכסים נדחסים בכל פעם שהם נדרשים, אפשר להשתמש בספריית הביניים compression. קודם כול מוסיפים אותו כ-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 יכולים להתארך באופן משמעותי.

דחיסת קבצים סטטיים באמצעות 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.

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

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

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

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

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

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

סיכום

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