Рубрики
Без рубрики

Express + Brotli + WebPack 🚀

Выражайте ❤️ в форме брольти с помощью WebPack, чтобы сохранить некоторых клиентов ⏰ и 💵

Автор оригинала: Rachit Gulati.

Давайте сжимаем вместе и узнаем, как Brotli может помочь нам увеличить производительность наших сайтов. Я реализовал его в одном из моих рабочих проектов. Итак, я просто подумал поделиться своим опытом со всеми.

Основные определения 📖.

Экспресс: Быстрый, неопиннованный, минималистский веб-каркас для Node.js. Броти: Это библиотека сжатия данных с открытым исходным кодом, разработанная Jyrki Alakuijala и Zoltán Szabadka. Он основан на современном варианте алгоритма LZ77, кодирования Хаффмана и моделирования контекста 2-го порядка. WebPack: Это модуль Bundler. Он принимает модули с зависимостями и генерирует статические активы, представляющие эти модули.

Давайте начнем с реального дерьма 💩 !!!

Были два способа реализации сжатия в Express напрямую (без какого-либо веб-сервера I.E: Nginx и т. Д.):

  1. Статическое здание сжатых файлов с помощью WebPack (любая другая задача Frontend или Builder) и обслуживает их по требованию по мере необходимости клиента.
  2. Динамическое здание сжатых файлов в течение времени выполнения (вы можете использовать требовать («сжатие»)) В Express для динамически компрессионных файлов и обслуживайтесь клиенту на лету. Я только реализовал статическое здание файлов. Так что давайте поговорим об этом более подробно.

Статическое сжатие с Express На первом шаге, который строит ваши пакеты , вы можете включить эти два плагина Сжатие - WebPack-Plugin и brotli-webpack-plugin Отказ

const CompressionPlugin = require('compression-webpack-plugin');
const BrotliPlugin = require('brotli-webpack-plugin');
module.exports = {
plugins: [
 new CompressionPlugin({
 asset: '[path].gz[query]',
 algorithm: 'gzip',
 test: /\.js$|\.css$|\.html$/,
 threshold: 10240,
 minRatio: 0.8
 }),
 new BrotliPlugin({
 asset: '[path].br[query]',
 test: /\.js$|\.css$|\.html$/,
 threshold: 10240,
 minRatio: 0.8
 })
]
}

Эти плагины будут генерировать как gzip, так и brotli file для всех ваших связков, т.е. если имя пучка ‘vendor_d0cfe49e718C136CFE49E718C136C61.js, вы получите Vendor_D0CFE49E718C136C661.js.gzip и vendor_d0c661.js.br в том же каталоге (давайте предположим, что это /dist/Static/ vendor_d0cfe49e718c1366c661.js. * На данный момент).

PS: Приведенный выше код будет генерировать только .gzip и .br, если minratio 0,8 достигается во время сжатия файлов. Таким образом, в случае очень маленьких файлов GZIP и BR файлы не будут сгенерированы. Причина в том, что время в сжимании и распарковании костлеее, чем фактический файл, обслуживаемый без сжатия.

Вам также может потребоваться установить публичный путь в конфигурации выхода WebPack в «/Static». Он будет указывает публичный адрес URL-адреса выходных файлов при упоминании в браузере. Что поможет нам фильтровать URL-адрес запроса и подавать файлы с помощью Express-Static-Gzip Fonly Static, если URL состоит из «/Static».

output: {
path: '/dist/static',
filename: '[name]_[chunkhash].js',
chunkFilename: '[id].[chunkhash].js',
publicPath: '/static/',
},

На втором этапе, который должен служить правильному файлу на основе входных заголовков из клиентского браузера Отказ Мы будем использовать Express-Static-Gzip

aaa.png.png.
var express = require("express");
var expressStaticGzip = require("express-static-gzip");
var app = express();
// app.use(express.static(path.join(__dirname))); This was used previously with express.
app.use("/", expressStaticGzip(path.join(__dirname), {
 enableBrotli: true
}));

Приведенный выше код является настройкой кода по умолчанию из «Express-Static-Gzip», но я хотел использовать только статические файлы из этой библиотеки. Если файл не существует, я хотел бросить ошибку, и мой код не должен идти дальше на другие маршруты. Итак, я только немного взломал в исходном коде и создал новый промежуточный файл Compression.js.

Ниже приведен взломанный код:

var express = require("express");
const expressStaticGzip = require('compression'); // compression.js gist is available on the github.
var app = express();
 
app.get('*', expressStaticGzip(path.join(__dirname), {
 urlContains: 'static/',
 fallthrough: false,
 enableBrotli: true,
}));

Есть три параметра, которые я использовал здесь

1 ** URLContains: ** Это проверит, будет ли оригинальный URL запроса «статический/» в нем. Тогда только подавать файлы через этот плагин, еще игнорируйте URL. 2. Блэст: Это должно быть ложно, чтобы бросить ошибку, если файл, который вы ищете, не существует в каталоге Path.join (__ dirname), и URL-адрес «URLContains». 3. Enablebrotli: Он проверит, доступен ли файл Brotli на Path.join (__ dirname), и соответствующие заголовки запрашиваются клиентом, затем обслуживают файл .br. Ниже приведен гид с компрессией .js. Я взломал с линии 59-65.

const mime = require('mime');
const serveStatic = require('serve-static');
const fileSystem = require('fs');

module.exports = expressStaticGzip;

/**
 * Generates a middleware function to serve static files.
 * It is build on top of the express.static middleware.
 * It extends the express.static middleware with
 * the capability to serve (previously) gziped files. For this
 * it asumes, the gziped files are next to the original files.
 * @param {string} rootFolder: folder to staticly serve files from
 * @param {{enableBrotli:boolean,
 * customCompressions:[{encodingName:string,fileExtension:string}],
 * indexFromEmptyFile:boolean}} options: options to change module behaviour
 * @returns express middleware function
 */
function expressStaticGzip(rootFolder, options) {
  options = options || {};
  if (typeof (options.indexFromEmptyFile) === 'undefined') options.indexFromEmptyFile = true;

    // create a express.static middleware to handle serving files
  const defaultStatic = serveStatic(rootFolder, options);
  const compressions = [];
  const files = {};

    // read compressions from options
  setupCompressions();

    // if at least one compression has been added, lookup files
  if (compressions.length > 0) {
    findAllCompressionFiles(fileSystem, rootFolder);
  }

  return function middleware(req, res, next) {
    changeUrlFromEmptyToIndexHtml(req);

        // get browser's' supported encodings
    const acceptEncoding = req.header('accept-encoding');

        // test if any compression is available
    const matchedFile = files[req.path];
    console.log(req.originalUrl, matchedFile);
    if (matchedFile) {
        // as long as there is any compression available for this
        // file, add the Vary Header (used for caching proxies)
      res.setHeader('Vary', 'Accept-Encoding');

                // use the first matching compression to serve a compresed file
      const compression =
            findAvailableCompressionForFile(matchedFile.compressions, acceptEncoding);
      if (compression) {
        convertToCompressedRequest(req, res, compression);
      }
    }

      // allways call the default static file provider
    defaultStatic(req, res, (err) => {
      if (err && (req.originalUrl.indexOf(options.urlContains) > -1)) {
        console.log('Hola', req.originalUrl, err);
        return res.status(404).json({ error: `No file found with ${req.originalUrl}` });
      }
      return next();
    });
  };

    /**
     * Reads the options into a list of available compressions.
     */
  function setupCompressions() {
        // register all provided compressions
    if (options.customCompressions && options.customCompressions.length > 0) {
      for (let i = 0; i < options.customCompressions.length; i += 1) {
        const customCompression = options.customCompressions[i];
        registerCompression(customCompression.encodingName, customCompression.fileExtension);
      }
    }

        // enable brotli compression
    if (options.enableBrotli) {
      registerCompression('br', 'br');
    }

        // gzip compression is enabled by default
    registerCompression('gzip', 'gz');
  }

    /**
     * Changes the url and adds required headers to serve a compressed file.
     * @param {Object} req
     * @param {Object} res
     */
  function convertToCompressedRequest(req, res, compression) {
    const type = mime.lookup(req.path);
    const charset = mime.charsets.lookup(type);
    let search = req.url.split('?').splice(1).join('?');

    if (search !== '') {
      search = `?${search}`;
    }

    req.url = req.path + compression.fileExtension + search;
    res.setHeader('Content-Encoding', compression.encodingName);
    res.setHeader('Content-Type', type + (charset ? `; charset=${charset}` : ''));
  }

    /**
     * In case it's enabled in the options and the
     * requested url does not request a specific file, "index.html" will be appended.
     * @param {Object} req
     */
  function changeUrlFromEmptyToIndexHtml(req) {
    if (options.indexFromEmptyFile && req.url.endsWith('/')) {
      req.url += 'index.html';
    }
  }

    /**
     * Searches for the first matching compression available from the given compressions.
     * @param {[Compression]} compressionList
     * @param {string} acceptedEncoding
     * @returns
     */
  function findAvailableCompressionForFile(compressionList, acceptedEncoding) {
    if (acceptedEncoding) {
      for (let i = 0; i < compressionList.length; i += 1) {
        if (acceptedEncoding.indexOf(compressionList[i].encodingName) >= 0) {
          return compressionList[i];
        }
      }
    }
    return null;
  }

    /**
     * Picks all files into the matching compression's file list. Search is done recursively!
     * @param {Object} fs: node.fs
     * @param {string} folderPath
     */
  function findAllCompressionFiles(fs, folderPath) {
    const filesMain = fs.readdirSync(folderPath);
        // iterate all files in the current folder
    for (let i = 0; i < filesMain.length; i += 1) {
      const filePath = `${folderPath}/${filesMain[i]}`;
      const stats = fs.statSync(filePath);
      if (stats.isDirectory()) {
                // recursively search folders and append the matching files
        findAllCompressionFiles(fs, filePath);
      } else {
        addAllMatchingCompressionsToFile(filesMain[i], filePath);
      }
    }
  }

    /**
     * Takes a filename and checks if there is any compression type matching the file extension.
     * Adds all matching compressions to the file.
     * @param {string} fileName
     * @param {string} fillFilePath
     */
  function addAllMatchingCompressionsToFile(fileName, fullFilePath) {
    for (let i = 0; i < compressions.length; i += 1) {
      if (fileName.endsWith(compressions[i].fileExtension)) {
        addCompressionToFile(fullFilePath, compressions[i]);
        return;
      }
    }
  }

    /**
     * Adds the compression to the file's list of available compressions
     * @param {string} filePath
     * @param {Compression} compression
     */
  function addCompressionToFile(filePath, compression) {
    const srcFilePath = filePath.replace(compression.fileExtension, '').replace(rootFolder, '');
    const existingFile = files[srcFilePath];
    if (!existingFile) {
      files[srcFilePath] = { compressions: [compression] };
    } else {
      existingFile.compressions.push(compression);
    }
  }

    /**
     * Registers a new compression to the module.
     * @param {string} encodingName
     * @param {string} fileExtension
     */
  function registerCompression(encodingName, fileExtension) {
    if (!findCompressionByName(encodingName)) {
      compressions.push(new Compression(encodingName, fileExtension));
    }
  }

    /**
     * Constructor
     * @param {string} encodingName
     * @param {string} fileExtension
     * @returns {encodingName:string, fileExtension:string,files:[Object]}
     */
  function Compression(encodingName, fileExtension) {
    this.encodingName = encodingName;
    this.fileExtension = `.${fileExtension}`;
  }

    /**
     * Compression lookup by name.
     * @param {string} encodingName
     * @returns {Compression}
     */
  function findCompressionByName(encodingName) {
    for (let i = 0; i < compressions.length; i += 1) {
      if (compressions[i].encodingName === encodingName) { return compressions[i]; }
    }
    return null;
  }
}

Анализ результатов:

Давайте сравним производительность сайта с Brotli или GZIP или просто несжатым заминированным файлами.

В Боге мы доверяем, все остальные приносят данные. -W. Эдвардс Деминг

Давайте копать ⛏ в анализ и найдите реальные числа. Поскольку это мое рабочее место веб-сайта, которое мы использовали для внутренней цели. Я не могу поделиться настоящим скриншотом здесь, но ниже реальные числа, которые я собирал при тестировании сайта на разных типах компрессиях.

aaaa.png.
  1. Brotli составляет ~ 8% ((7.24-6,67)/7.24), эффективно, чем GZIP и 65,7% (19,48-6,67)/19,48), эффективно, чем несжатый файл. Если браузер не сможет служить Brotli, у нас есть загрязнительный в GZIP, который составляет 62% (19,48-7.24)/19,48), эффективнее несжатого файла. Так что здесь у нас выиграют ситуацию.
  2. Теперь давайте проанализируем размер. Броти – это ((586-458)/586) ~ 21,85% эффективно, чем gzip, и оно есть ((2.5 1024-458)/2.5 1024) ~ 82,1% эффективно, чем несжатые файлы. Таким образом, много пропускной способности можно сохранить с помощью сжатия бротлей. Некоторые данные для медленной сети 3G:
Aaaaa.png.

Спасибо всем за чтение до сих пор. Если вам это нравится и хочу, чтобы я написал больше. Пожалуйста, дайте мне знать.