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

Michael DiBlasio
Michael DiBlasio

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

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

מדידה

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

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

ב-Codelab הקודם בנושא הקטנה ודחיסה של מטענים ייעודיים (payload) ברשת, הקטנו את הגודל של main.js מ-225KB ל-61.6KB. ב-Codelab הזה תלמדו איך דחיסת Brotli יכולה להקטין את גודל החבילה עוד יותר.

דחיסת Brotli

‫Brotli הוא אלגוריתם דחיסה חדש יותר, שיכול לספק תוצאות דחיסת טקסט טובות יותר מ-gzip. לפי CertSimple, הביצועים של Brotli הם:

  • ‫14% קטן יותר מ-gzip ל-JavaScript
  • קטן ב-21% מ-gzip ל-HTML
  • ‫17% פחות מ-gzip לשירות CSS

כדי להשתמש ב-Brotli, השרת צריך לתמוך ב-HTTPS. ‫Brotli נתמך בכל הדפדפנים המודרניים. דפדפנים שתומכים ב-Brotli יכללו את br בכותרות Accept-Encoding:

Accept-Encoding: gzip, deflate, br

כדי לדעת באיזה אלגוריתם דחיסה נעשה שימוש, אפשר להשתמש בשדה Content-Encoding בכרטיסייה Network (רשת) בכלים למפתחים ב-Chrome (Command+Option+I או Ctrl+Alt+I):

חלונית "רשת". בעמודה Content-encoding מוצגים קידודים שמשמשים לנכסים שונים, כולל gzip ו-brotli‏ (br).

איך מפעילים את Brotli

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

דחיסה דינמית

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

יתרונות

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

חסרונות

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

דחיסה דינמית באמצעות 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 ולהשתמש בתווכה (middleware) express.static כדי לטעון את כל קובצי ה-HTML, ה-JS וה-CSS הסטטיים ב-public/directory (והקבצים האלה נוצרים על ידי webpack בכל build).

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

"devDependencies": {
  // ...
  "shrink-ray": "^0.1.3"
},

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

const express = require('express');
const shrinkRay = require('shrink-ray');

ולהוסיף אותו כ-middleware לפני ש-express.static נטען:

// ...
const app = express();

// Compress all requests
app.use(shrinkRay());
app.use(express.static('public'));

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

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

עכשיו אפשר לראות שההגדרה brotli חלה מ-bz בכותרת של Content-Encoding. main.bundle.js הצטמצם מ-‎225KB ל-‎53.1KB! זה קטן בכ-14% בהשוואה ל-gzip (61.6KB).

דחיסה סטטית

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

יתרונות

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

חסרונות

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

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

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

קודם כול, מוסיפים אותו בתור devDependency ב-package.json:

"devDependencies": {
  // ...
 "brotli-webpack-plugin": "^1.1.0"
},

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

var path = require("path");

//...
var BrotliPlugin = require('brotli-webpack-plugin');

וכוללים אותו במערך הפלאגינים:

module.exports = {
  // ...
  plugins: [
    // ...
    new BrotliPlugin({
      asset: '[file].br',
      test: /\.(js)$/
    })
  ]
},

מערך הפלאגין משתמש בארגומנטים הבאים:

  • ‫asset: שם נכס היעד.
  • ‫[file] מוחלף בשם הקובץ המקורי של הנכס.
  • ‫test: כל הנכסים שתואמים לביטוי הרגולרי הזה (כלומר, נכסי JavaScript שמסתיימים ב-.js) מעובדים.

לדוגמה, השם main.js ישתנה ל-main.js.br.

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

  1. לוחצים על הכפתור כלים.
  2. לוחצים על הכפתור Console.
  3. במסוף, מריצים את הפקודות הבאות כדי לעבור לספרייה public ולראות את כל הקבצים שבה:
cd public
ls -lh
גודל החבילה עם דחיסת Brotli סטטית

הגרסה הדחוסה של החבילה בפורמט brotli,‏ main.bundle.js.br, נשמרת עכשיו גם כאן, והיא קטנה יותר ב-76% בערך (225KB לעומת 53KB) מ-main.bundle.js.

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

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

app.get('*.js', (req, res, next) => {
  req.url = req.url + '.br';
  res.set('Content-Encoding', 'br');
  res.set('Content-Type', 'application/javascript; charset=UTF-8');
  next();
});

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

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

  • הגדרת '*.js' כארגומנט הראשון פירושה שהפעולה הזו פועלת בכל נקודת קצה (endpoint) שמופעלת כדי לאחזר קובץ JS.
  • בתוך הקריאה החוזרת, .br מצורף לכתובת ה-URL של הבקשה וכותרת התגובה Content-Encoding מוגדרת ל-br.
  • הכותרת Content-Type מוגדרת ל-application/javascript; charset=UTF-8 כדי לציין את סוג ה-MIME.
  • לבסוף, next() מבטיח שהרצף ימשיך לכל קריאה חוזרת (callback) שעשויה להיות הבאה בתור.

יכול להיות שדפדפנים מסוימים לא תומכים בדחיסת brotli. לכן, לפני שמחזירים את הקובץ שנדחס באמצעות brotli, צריך לוודא שיש תמיכה ב-brotli. כדי לעשות את זה, בודקים שכותרת הבקשה Accept-Encoding כוללת את br:

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

app.get('*.js', (req, res, next) => {
  if (req.header('Accept-Encoding').includes('br')) {
    req.url = req.url + '.br';
    console.log(req.header('Accept-Encoding'));
    res.set('Content-Encoding', 'br');
    res.set('Content-Type', 'application/javascript; charset=UTF-8');
  }

  next();
});

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

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

גודל החבילה הוא 53.1KB (במקום 225KB)

הצלחת! השתמשתם בדחיסת Brotli כדי לדחוס עוד יותר את הנכסים שלכם.

סיכום

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