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

Michael DiBlasio
Michael DiBlasio

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

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

מדידה

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

  1. לוחצים על Remix to Edit כדי לאפשר עריכה של הפרויקט.
  2. כדי לראות תצוגה מקדימה של האתר, מקישים על הצגת האפליקציה. לאחר מכן מקישים על מסך מלא מסך מלא.

בקודלאב הקודם בנושא צמצום ודחיסה של מטענים ייעודיים (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 בכרטיסייה 'רשת' בכלים למפתחים ב-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 ושימוש ב-middlware של 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'));

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

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

עכשיו אפשר לראות בכותרת Content-Encoding שהכלל brotli מיושם מ-bz. 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 Console כדי לראות מה נמצא בספרייה public/ הסופית שמוצגת על ידי שרת Node.

  1. לוחצים על הלחצן כלים.
  2. לוחצים על הלחצן מסוף.
  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' כארגומנט הראשון פירושו שהקוד הזה יפעל בכל נקודת קצה שמופעל בה אחזור של קובץ 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 כדי לדחוס עוד יותר את הנכסים שלכם.

סיכום

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