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

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

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

מדידה

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

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

באפליקציה הזו, שכלולה גם ב-"Remove unused code", תוכלו להצביע לחתלתול האהוב עליכם. 🐈

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

  1. לוחצים על 'Control+Shift+J' (או 'Command+Option+J' ב-Mac) כדי לפתוח את כלי הפיתוח.
  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++;}}

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

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

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

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

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

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

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

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

תגובה מינימלית

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

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

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

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

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

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

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

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

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

דחיסה

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

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

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

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

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

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

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

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

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

דחיסה דינמית

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

יתרונות

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

חסרונות

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

דחיסה דינמית עם צומת/אקספרס

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

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).

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

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

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

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

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

סיכום

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