Экспресс + Бротли + Вебпак 🚀
Давайте вместе сожмем и узнаем, как Brotli может помочь нам повысить производительность наших веб-сайтов. Я реализовал это в одном из своих рабочих проектов. Так что я просто решил поделиться своим опытом со всеми.
Основные определения 📖
Выражать: Быстрый, бескомпромиссный, минималистичный веб-фреймворк для Node.js.
Бротли: Это библиотека сжатия данных с открытым исходным кодом, разработанная Юрки Алакуйала и Золтаном Шабадкой. Он основан на современном варианте алгоритма LZ77, кодировании Хаффмана и контекстном моделировании 2-го порядка.
Веб-пакет: Это сборщик модулей. Он берет модули с зависимостями и генерирует статические ресурсы, представляющие эти модули.
Начнем с настоящего дерьма 💩 !!!
Было два способа реализовать сжатие в Express напрямую (без какого-либо веб-сервера, например: nginx и т. д.):
- Статическое создание сжатых файлов с помощью Webpack (любого другого исполнителя задач или компоновщика) и предоставление их по требованию клиента.
- Динамическое построение сжатых файлов во время выполнения (Вы можете использовать требуют(‘сжатие’)) 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
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 или просто с несжатыми минифицированными файлами.
Мы верим в Бога, все остальные приносят данные. -В. Эдвардс Деминг
Давайте углубимся в анализ и найдем реальные цифры. Так как это мой рабочий сайт, который мы использовали для внутренних целей. Я не могу поделиться настоящим скриншотом здесь, но ниже приведены реальные цифры, которые я собрал, тестируя веб-сайт на различных типах сжатия.
- 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.
- Теперь давайте проанализируем размер. Brotli (( 586–458)/586) ~ 21,85% эффективнее GZIP и (( 2,51024–458)/2,51024)~82,1% эффективнее несжатых файлов. Таким образом, можно сэкономить большую часть пропускной способности, используя сжатие Brotli.
Некоторые данные для медленной сети 3G:
Спасибо всем за чтение до сих пор. Если вам это нравится и вы хотите, чтобы я написал больше. Пожалуйста, дай мне знать.