Некоторое время назад писал сайт с бэкендом на Express/Node.JS. Возникла проблема с минификацией ответов. Нашел много готовых пакетов, но у всех была проблема — не минифицировался html после шаблонов. В итоге принял решение написать свой маленький и родной велосипед — библиотеку web-minify, позволяющую встроить хук перед отправкой клиенту и модифицировать ответ.

Установка пакета


npm i @dmitriym09/web-minify --save

Думаю, самое лучшее описание библиотеки для разработчика — пример кода=)

Пример


web-minify — middleware-функция:

const htmlminify = require('html-minifier').minify;

const csso = require('csso').minify;
const postcss = require('postcss');
const precss = require('precss');
const autoprefixer = require('autoprefixer');

const minify = require('web-minify');

app.use(minify([
  {
    contentType: /css/,
    minify: async (data, req, res) => {
      let resData = (await postcss([precss, autoprefixer]).process(data, { from: undefined })).css;
      
      resData = csso(resData).css;

      return resData;
    }
  }
]));

В данном примере будут перехватываются все ответы с Content-Type, содержащие подстроку «css». Тело ответа обрабатывается с помощью csso, postcss, precss, autoprefixer. В параметре contentType передается String (будет искаться вхождение String.prototype.indexOf()) или RegExp (RegExp.prototype.test()). Параметр minify — функция Function(data:String, req:Request, res:Response), должна возвращать String с новым телом или Promise, который в свою очередь разрешается String. При неотловленом исключении клиент получит ответ 500.

Как уже сказал, большинство существующих популярных библиотек с похожим функционалом хорошо минифицирует статические файлы. Однако минификация сгенерированных в коде (например html шаблонизатором) ответов не работает. Одна из проблем — ответ может отправляться частями, а для обработки обычно нужны полные данные. Соответственно нужно перехватывать все отправки пользователю, собирать и уже в конце обрабатывать и отсылать. Это нужно учитывать при использовании web-minify: тот терабайтный файл, который вы хотите отправить клиенту и который попадает под contentType, накапливаться в памяти.

Примеры


Минификация HTML с помощью html-minifier из юнит-тестов


const htmlminify = require('html-minifier').minify;
it('HTML', (done) => {
	const app = createServer([minify([
		{
			contentType: 'html', 
			minify: (data) => { 
				let res = htmlminify(data, {
					removeAttributeQuotes: true,
					collapseWhitespace: true,
					conservativeCollapse: false,
					decodeEntities: true,
					keepClosingSlash: false,
					preserveLineBreaks: false,
					preventAttributesEscapin,
					processConditionalComments: true,
					removeAttributeQuotes: true,
					removeComments: true,
					trimCustomFragments: true,
					useShortDoctype: true
	    		});
	   			return res;
	  		}
		}
	])], function(req, res) {
		res.setHeader('Content-Type', 'text/html; charset=utf-8');
	    res.end(`<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="utf-8">

</head>

<body>
<h1>Test</h1>

<p>Test</p>
</body>`);
	});


	request(app)
	  .get('/')
	  .set('Accept', 'text/html; charset=utf-8')
	  .expect('Content-Type', 'text/html; charset=utf-8')
	  .expect('<!doctype html><html lang=en><head><meta charset=utf-8></head><body><h1>Test</h1><p>Test</p></body></html>')
	  .expect(200)
	  .end(done)
});

Модификации JSON и кода ответа с возвратом Promise из юнит-тестов


it('JSON', (done) => {
	const app = createServer([minify([
		{
			contentType: /json/,
			minify: (data, req, res) => {
				return new Promise(function(resolve, reject) {
					try {
						res.statusCode = 456;
						let o = JSON.parse(data);
						o.dt = new Date('2018-09-28T11:05:13.492Z') 
						resolve(JSON.stringify(o))
					}
					catch(exc) {
						reject(exc)
					}
				})
				
			}
		}
	])], function(req, res) {
		res.setHeader('Content-Type', 'application/json; charset=utf-8');
	    res.end(JSON.stringify({a: 12}));
	});

	request(app)
	  .get('/')
	  .set('Accept', 'applicatio3n/json; charset=utf-8')
	  .expect('Content-Type', 'application/json; charset=utf-8')
	  .expect('{"a":12,"dt":"2018-09-28T11:05:13.492Z"}')
	  .expect(456)
	  .end(done)
});

Web-minify доступна на github и в npm под лицензией MIT.

Спасибо за внимание! Критика, предложения и комментарии приветствуются!

Комментарии (18)


  1. Koneru
    04.10.2018 19:27
    +2

    Извините, за банальный вопрос, но зачем? Почему нельзя выполнить нужные операции один раз?


    1. dmitriym09 Автор
      04.10.2018 20:13

      Ну статику конечно можно и нужно пожать 1 раз и раздавать. Но есть же и динамический контент. Например html, созданный шаблонами. Конечно можно и его жать сразу в шаблонизаторе, но есть такой подход.


      1. kalyukdo
        05.10.2018 09:58

        Это должен делать какойнить дженкинс и складывать на сервер, зачем заставлять ноду это делать? зачем тянуть кучу лишних зависимостей в продакшин?


        1. dmitriym09 Автор
          05.10.2018 10:05

          Как какойнить дженкинс будет обрабатывать динамически создаваемый контент и складывать на сервер?


          1. kalyukdo
            05.10.2018 10:44

            Извините если я не понял вашу идею, но тогда давайте разберемся, если у Вас полная динамика css/html то минификацию нужно делать сразу на момент сохранения данных, но никак ни на момент каждого запроса к серверу, да я понимаю что у вас там возможно есть кэш, но при каждом запросе проверять данные из кеша не айс когда можно сразу nginx натравить на ваши динамические html/css и все.
            Или же у вас css зависит от того какой пришел запрос?


            1. dmitriym09 Автор
              05.10.2018 11:07

              Есть статика, как, наверное, у любого сайта. Они собирается webpack. Сами html страницы генерятся при запросе с помощью шаблонов, в проекте о котором и шла в частности используется ejs. Можно конечно заморочиться и сделать так, чтобы после шаблонизатора получался уже минифицированный ответ. Но там есть нюансы и на мой вкус лучше обрабатывать уже полностью собранный ответ. Кеш конечно используется — я ставлю перед node-приложением nginx. Вообще эта библиотека не обязательно должна использоваться для минификации чего-то. Она просто позволяет хукнуть ответ так, чтобы было полное тело и что то можно сделать с ним перед отправкой. Можно, например, вместо webpack собирать бандлы прям на приложении при запросе, а потом кешировать их уже на проксире. Конечно тут куча сложностей и в большинстве случаев так делать не надо, но я вполне могу представить себе кейс где бы это было хорошо. Ну например код после какого-либо конструктора сайта. Или же нужно менять то, что генерит какая-нибудь 3rd-либа без возможности влезть в нее. Или же у вас есть кеча inline-стилей, которые можно собрать во внешний бандл. Естественно я решал не каждодневную задачу — она не стоит перед всеми, раз нет в стандартной реализации. В общем я решал проблему, не нашел удобного мне способа в стандартной реализации, навелосипедил и решил выложить в opensource. Может быть кому-нибудь поможет когда-нибудь. Естественно нужно думать головой когда и как ее использовать, чтобы было лучше, а не хуже.


            1. Rahman
              05.10.2018 12:33

              на практике пришли к выводу, что подобная оптимизация не входит в задачи приложения. остановились на PageSpeed, там оптимизация намного глубже чем просто пожать html/css/картинки.


              1. dmitriym09 Автор
                05.10.2018 12:34

                А что с динамическим контентом? Как его жмете?


                1. Rahman
                  05.10.2018 12:38

                  PageSpeed работает на уровне запросов, как прокси в nginx. Все равно как был получен контент, статический файл или на лету сгенерированный. Приложение отдает ответ на запрос, PageSpeed его обрабатывает, кеширует и т.д.


                  1. dmitriym09 Автор
                    05.10.2018 13:10

                    Ну да, это удобно. А какие есть ограничения? Сколько запросов максимум? Платно\бесплатно?

                    Когда используешь PageSpeed для статистики сайта то он вроде кешируешь на какое-то время — сразу после внесения изменений не видит их. Как это решено через их API? На что он ориентируется для кеша?

                    Но нужно учитывать, что не все ответы можно отдавать 3-ей стороне. Бывает содержатся и приватные данные. Конечно их можно выдрать и отправлять отдельным запросом.

                    Например, я воспользовался данной библиотекой для того, чтобы после после конструктора сайта, создающего довольно трешовый код, выдерать inline-стили и потом оптимизировать их. Что-то уникальное часто руками приходится писать.

                    В общем любая проблема решается 100500 способами и каждый разработчик должен сам думать. А наличие большого разнообразия возможностей для решения — это хорошо.


                    1. Rahman
                      05.10.2018 15:49

                      Несмотря на похожее название, между PageSpeed Insights (сервис оценки производительности страницы) и developers.google.com/speed/pagespeed/module достаточно большая разница. То, что я описываю, ставится на ваш локальный apache/nginx. Ни о api, ни о ограничении количества запросов речь не идет. Просто прокси + какая то собственная админка для управления (статистика, кеши и вот это все)

                      Относительно неплохо срабатывание не просто минимизация html, но и включение внешних javascript/css файлов непосредственно в страницу, сборка нескольких файлов в бандлы, оптимизация картинок и т.д. Более подробно лучше посмотреть в их документации.

                      А наличие большого разнообразия возможностей для решения — это хорошо.

                      Несомненно данное решение имеет право на жизнь. Модель middleware, встроенного в собственное приложение позволяет работать в любом окружении, где бы приложение не запускалось.

                      Моя мысль заключается в том, что подобная оптимизация для серьезного продукта более уместна вне приложения, а именно настройками окружения.


  1. kolesoffac
    05.10.2018 09:02

    Откройте для себя spa и ssr.


    1. dmitriym09 Автор
      05.10.2018 09:27

      Открыл уже давно — есть такие проекты у меня. Но считаю, что не серебрянная пуля и не панацея. Нужно исходить и задач и кейса. Обычно эти 3-х буквенные сокращения идут рядом с реактивностью, а она не всегда бывает нужна. Кроме того, не все серверные рендеры дают минифицированный html.


  1. eugef
    05.10.2018 09:11

    Вместо того, чтобы экономить на спичках и удалять из HTML и JSON пробелы и кавычки, лучше сжать ответ с помощью gzip. К тому же так можно сжимать потоки и не надо накапливать весь ответ (тем более 1ТБ в памяти).

    Посмотрите например www.npmjs.com/package/compression


    1. dmitriym09 Автор
      05.10.2018 09:22

      «Экономия на спичках» дает в некоторых ситуациях сжатие до 10%.Cжатый html — это еще и плюс для SEO. Кроме того, одно другому не мешает, а дополняет: можно сначала пожать код, а потом gzip использовать. Я кстати так и делаю. Только я ставлю перед node приложением кеширующий проксер (nginx например), который и gzip-ает ответ клиенту. Кавычки из JSON не удаляются, а то это будет уже невалидный json)


      1. eugef
        05.10.2018 12:27

        Cжатый html — это еще и плюс для SEO

        Можете предоставить ссылку на пруф, что именно сжатый HTML влияет на СЕО?
        Где указано, что влияет именно HTML без лишних пробелов и кавычек, а не просто более оптимальный HTML без лишних тегов или более правильный семантически.

        Кроме того, одно другому не мешает, а дополняет: можно сначала пожать код, а потом gzip использовать.

        Давайте проведем эксперимент. Возьмем довольно большую страницу, например habr.com/post/425351 и сожмем ее по разному:
        • Исходный размер HTML: 582KB
        • Размер после html-minifier: 543KB (-7%)
        • Размер после html-minifier и gzip: 78KB (-87%)
        • Размер после одного gzip: 81KB (-86%)

        Т.е. при условии, что HTML сжимается с помощь gzip, минификация дает крайне минимальный эффект (1% всего) — вот это я и называю «экономия на спичках».

        Лучше процессорное время потратить на обработку другого запроса, чем минифицировать HTML (а тем более JSON)

        я ставлю перед node приложением кеширующий проксер (nginx например)

        Это, безусловно, правильное решение.


        1. dmitriym09 Автор
          05.10.2018 12:58

          Можете предоставить ссылку на пруф, что именно сжатый HTML влияет на СЕО?
          Где указано, что влияет именно HTML без лишних пробелов и кавычек, а не просто более оптимальный HTML без лишних тегов или более правильный семантически.


          Не было такого утверждения. Было написано
          Cжатый html — это еще и плюс для SEO.

          Не раскрывалось в чем плюс.

          … минифицировать HTML (а тем более JSON)

          Где здесь минифицируется JSON??

          Лучше процессорное время потратить на обработку другого запроса

          И это лично ваше мнение. Очень сильно зависит от нагрузки и целей. Я думаю каждый разработчик сам должен принимать решение.

          Т.е. при условии, что HTML сжимается с помощь gzip, минификация дает крайне минимальный эффект (1% всего) — вот это я и называю «экономия на спичках».

          Опять все зависит от ситуации. Может кто-то хочет уместить ответ в минимальное количество tcp пакетов и 1% может повлиять на результат.

          Вообще здесь приведены абстрактные решения в вакууме для демонстрации функциональности. Например, я воспользовался данной библиотекой для того, чтобы после после конструктора сайта, создающего довольно трешовый код, выдерать inline-стили и потом оптимизировать их. Естественно нужно думать головой когда и как что использовать, чтобы было лучше, а не хуже. Каждый разработчик принимает решение в зависимости от многих критериев для свое ситуации. И я считаю, что лишний инструмент, расширяющий возможности — это хорошо.


          1. eugef
            05.10.2018 23:54

            Не было такого утверждения. Было написано
            «Cжатый html — это еще и плюс для SEO.»


            Ok, раскройте, в чем именно плюс. И желательно с ссылкой на пруф.