Экспресс + Бротли + Вебпак 🚀

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

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

Выражать: Быстрый, бескомпромиссный, минималистичный веб-фреймворк для Node.js.
Бротли: Это библиотека сжатия данных с открытым исходным кодом, разработанная Юрки Алакуйала и Золтаном Шабадкой. Он основан на современном варианте алгоритма LZ77, кодировании Хаффмана и контекстном моделировании 2-го порядка.
Веб-пакет: Это сборщик модулей. Он берет модули с зависимостями и генерирует статические ресурсы, представляющие эти модули.

Начнем с настоящего дерьма 💩 !!!

Было два способа реализовать сжатие в Express напрямую (без какого-либо веб-сервера, например: nginx и т. д.):

  1. Статическое создание сжатых файлов с помощью Webpack (любого другого исполнителя задач или компоновщика) и предоставление их по требованию клиента.
  2. Динамическое построение сжатых файлов во время выполнения (Вы можете использовать требуют(‘сжатие’)) in express для динамического сжатия файлов и предоставления их клиенту на лету.
    Я реализовал только статическое построение файлов. Итак, давайте поговорим об этом подробнее.

Статическое сжатие с помощью Express
На первом этапе, который создает ваши пакетывы можете включить эти два плагина compression-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 для всех ваших пакетов, т. е. если имя пакета «vendor_d0cfe49e718c1366c661.js», вы получите файлы vendor_d0cfe49e718c1366c661.js.gzip и vendor_d0cfe49e718c1366c661.js.br в одном и том же каталоге (допустим, это /расстояние/статический/ vendor_d0cfe49e718c1366c661.js.* на данный момент).

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

Вам также может потребоваться установить общедоступный путь в конфигурации вывода веб-пакета на «/ static». Он будет указывать общедоступный URL-адрес выходных файлов при ссылке в браузере. Это поможет нам отфильтровать URL-адрес запроса и обслуживать файлы с помощью express-static-gzip fonly static, если URL-адрес состоит из «/ static».

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

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

ааа.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. включитьБротли: Он проверит, доступен ли файл 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 или просто с несжатыми минифицированными файлами.

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

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

аааа.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) эффективнее, чем несжатый файл. Итак, у нас есть ситуация Win Win.
  2. Теперь давайте проанализируем размер. Brotli (( 586–458)/586) ~ 21,85% эффективнее GZIP и (( 2,51024–458)/2,51024)~82,1% эффективнее несжатых файлов. Таким образом, можно сэкономить большую часть пропускной способности, используя сжатие Brotli.
    Некоторые данные для медленной сети 3G:

ааааа.png

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

Похожие записи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *