CSSO — история ускорения
Роман Дворнов
Avito
HolyJS
Санкт-Петербург, 2016
Работаю в Avito
Делаю SPA
Автор basis.js
Мейнтенер CSSO
За любую движуху, 

кроме голодовки ;)
ВремясжатияCSS(600Kb)
500 ms
1 000 ms
1 500 ms
2 000 ms
2 500 ms
3 000 ms
3 500 ms
4 000 ms
4 500 ms
5 000 ms
5 500 ms
6 000 ms
Версия CSSO
1.4.0 1.5.0 1.6.0 1.7.0 1.8.0 2.0
1 050 ms
clean-css
Изменение скорости CSSO
csso
500 ms
cssnano
23 250 ms
Почему это важно?
5
clean-css 3.4.16 cssnano 3.6.2 csso 2.1.1
ноябрь 2015
602 245 байт
430 240
2 039 ms
439 210
41 355 ms
435 629
714 ms
апрель 2016
822 021 байт
587 906
2 546 ms
604 167
76 665 ms
595 834
793 ms
июнь 2016
883 498 байт
632 007
2 588 ms
648 579
94 867 ms
639 495
803 ms
Время минификации CSS
Основной CSS файл ActiAgent.ru
Тренд изменения времени от размера CSSТрендизмененийразмераивремени
100 %
120 %
140 %
160 %
180 %
200 %
220 %
240 %
Размер CSS
602 245 822 021 883 498
размер CSS clean-css cssnano csso
229%
146%
127%
112%
7
Новые продвинутые оптимизации
(usage data, rename, etc)
245 823
Время
Размер (байт) 639 495
803 ms 2 513 ms
Лучше сжатие, но требует 2-3х больше времени
8
clean-css 3.4.16 cssnano 3.6.2 csso 2.1.1
15,5 sec 9 min 29 sec 4,8 sec
Таких файлов в проекте несколько (6 больших)
время минификации всех файлов
8
clean-css 3.4.16 cssnano 3.6.2 csso 2.1.1
15,5 sec 9 min 29 sec 4,8 sec
Таких файлов в проекте несколько (6 больших)
время минификации всех файлов
clean-css 3.4.16 cssnano 3.6.2 csso 2.1.1
не поддерживается
(≈46,5 sec)
не поддерживается
(≈28 min 27 sec) ~15 sec
+ продвинутые оптимизации
9
CSS
сжатый
CSS
Процесс минификации
9
CSS
сжатый
CSS
Парсер
Процесс минификации
9
CSS
сжатый
CSS
ASTПарсер
Процесс минификации
9
CSS
сжатый
CSS
ASTПарсер
Компрессор
Процесс минификации
9
CSS
сжатый
CSS
AST
AST
Парсер
Компрессор
Процесс минификации
9
CSS
сжатый
CSS
AST
AST
Парсер
Компрессор
Транслятор
Процесс минификации
9
CSS
сжатый
CSS
AST
AST
Парсер
Компрессор
Транслятор
Процесс минификации
Минификатор
Парсер
Основные шаги
• Токенизация
• Построение дерева (лексер)
11
Токенизация
13
• whitespaces – [ nrtf]+
• keyword – [a-zA-aZ…]+
• number – [0-9]+
• string – "string" или 'string'
• comment – /* comment */
• punctuation – [;,.#{}[]()…]
Разбиение текста на токены
14
.foo {
width: 10px;
}
[
'.', 'foo', ' ', '{',
'n ', 'width', ':',
' ', '10', 'px', ';',
'n', '}'
]
Низкоуровневые штуки
16
for (var i = 0; i < str.length; i++) {
var ch = str.charAt(i);
var chNext = str.charAt(i + 1);
if (ch === '/' && chNext === '*') {
result.push(readComment());
} else if (ch >= '0' && ch <= '9') {
result.push(readNumber());
} ...
}
Ключевой цикл токенайзера
17
for (var i = 0; i < str.length; i++) {
var ch = str.charAt(i);
var chNext = str.charAt(i + 1);
if (ch === '/' && chNext === '*') {
result.push(readComment());
} else if (ch >= '0' && ch <= '9') {
result.push(readNumber());
} ...
}
Ключевой цикл токенайзера
18
for (var i = 0; i < str.length; i++) {
var ch = str.charAt(i);
var chNext = str.charAt(i + 1);
if (ch === '/' && chNext === '*') {
result.push(readComment());
} else if (ch >= '0' && ch <= '9') {
result.push(readNumber());
} ...
}
Ключевой цикл токенайзера
19
for (var i = 0; i < str.length; i++) {
var ch = str.charAt(i);
var chNext = str.charAt(i + 1);
if (ch === '/' && chNext === '*') {
result.push(readComment());
} else if (ch >= '0' && ch <= '9') {
result.push(readNumber());
} ...
}
Ключевой цикл токенайзера
20
for (var i = 0; i < str.length; i++) {
var ch = str.charAt(i);
var chNext = str.charAt(i + 1);
if (ch === '/' && chNext === '*') {
result.push(readComment());
} else if (ch >= '0' && ch <= '9') {
result.push(readNumber());
} ...
}
Ключевой цикл токенайзера
Обычный код – простор для
потери быстродействия
21
Строки vs. числа
23
for (var i = 0; i < str.length; i++) {
var ch = str.charAt(i);
var chNext = str.charAt(i + 1);
if (ch === '/' && chNext === '*') {
result.push(readComment());
} else if (ch >= '0' && ch <= '9') {
result.push(readNumber());
} ...
}
Сравнение символов
24
• Строка – последовательность чисел
• Извлечение символа = получение новой строки
• Сравнение символов ≈ сравнение строк
Нужно помнить
25
!!!
26
// выносим коды символов в константы
var STAR = 42;
var SLASH = 47;
var ZERO = 48;
var NINE = 57;
...
Делаем быстрее
27
for (var i = 0; i < str.length; i++) {
var code = str.charCodeAt(i);
var codeNext = str.charCodeAt(i + 1);
if (code === SLASH && codeNext === STAR) {
result.push(readComment());
} else if (code >= ZERO && code <= NINE) {
result.push(readNumber());
} ...
}
charCodeAt() + числовые константы
28
✔
Нет больше
StringCharFromCode
✔
Всегда сравнение
чисел
28
✔
Нет больше
StringCharFromCode
✔
Всегда сравнение
чисел
???
Загрузка значения
перед сравнением
Используй const, Люк!
29
var STAR = 42;
var SLASH = 47;
var ZERO = 48;
var NINE = 57;
...
const STAR = 42;
const SLASH = 47;
const ZERO = 48;
const NINE = 57;
...
30
WTF?
– Вячеслав Егоров
“... [const] это все-таки неизменяемая привязка
переменной к значению ...
С другой стороны виртуальная машина может и
должна бы использовать это самое свойство
неизменяемости ...
V8 не использует, к сожалению.”
31
habrahabr.ru/company/jugru/blog/301040/#comment_9622474
32
for (var i = 0; i < str.length; i++) {
var code = str.charCodeAt(i);
var codeNext = str.charCodeAt(i + 1);
if (code === 47 && codeNext === 42) {
result.push(readComment());
} else if (code >= 48 && code <= 57) {
result.push(readNumber());
} ...
}
Но мы люди упоротые упорные
32
for (var i = 0; i < str.length; i++) {
var code = str.charCodeAt(i);
var codeNext = str.charCodeAt(i + 1);
if (code === 47 && codeNext === 42) {
result.push(readComment());
} else if (code >= 48 && code <= 57) {
result.push(readNumber());
} ...
}
Но мы люди упоротые упорные
Положительный эффект не замечен…
33
356ms – chatAt()
307ms – charCodeAt()
Результаты
на ~14% быстрее
Осторожней со строками
35
for (var i = 0; i < str.length; i++) {
var code = str.charCodeAt(i);
var codeNext = str.charCodeAt(i + 1);
if (code === SLASH && codeNext === STAR) {
result.push(readComment());
} else if (code >= ZERO && code <= NINE) {
result.push(readNumber());
} ...
}
Избегаем лишних чтений из строки
36
for (var i = 0; i < str.length; i++) {
var code = str.charCodeAt(i);
if (code === SLASH) {
var codeNext = str.charCodeAt(i + 1);
if (codeNext === STAR) {
result.push(readComment());
continue;
}
}
...
}
Избегаем лишних чтений из строки
307ms → 292 ms (-5%)
37
for (var i = 0; i < str.length; i++) {
var code = str.charCodeAt(i);
if (code === SLASH) {
var codeNext = str.charCodeAt(i + 1);
if (codeNext === STAR) {
result.push(readComment());
continue;
}
}
...
}
Опасная операция!
38
Попытка чтения за пределами 

строки или массива 

приводит к деоптимизации
39
var codeNext = 0;
if (i + 1 < str.length) {
codeNext = str.charCodeAt(i + 1);
}
Избегаем выхода за пределы строки
292ms → 69 ms (в 4 раза быстрее!)
Меньше сравнений
41
if (code >= ZERO && code <= NINE) {
// число
} else if (code === SINGLE_QUOTE ||
code === DOUBLE_QUOTE) {
// строка
} else if (code === SPACE || code === N ||
code === R || code === F) {
// whitespace
} …
Много сравнений
42
var PUNCTUATION = {
9: 'Tab', // 't'
10: 'Newline', // 'n'
...
126: 'Tilde' // '~'
};
...
} else if (code in PUNCTUATION) {
// символ пунктуации
} ...
Проверка в словаре – очень дорого
43
var CATEGORY_LENGTH = Math.max.apply(null, Object.keys(PUNCTUATION)) + 1;
var CATEGORY = new Uint32Array(CATEGORY_LENGTH);
Object.keys(PUNCTUATION).forEach(function(key) {
CATEGORY[key] = PUNCTUATOR;
});
for (var i = ZERO; i <= NINE; i++) {
CATEGORY[i] = DIGIT;
}
CATEGORY[SINGLE_QUOTE] = STRING;
CATEGORY[DOUBLE_QUOTE] = STRING;
CATEGORY[SPACE] = WHITESPACE;
...
Создаем карту категорий
44
switch (code < CATEGORY_LENGTH ? CATEGORY[code] : 0) {
case DIGIT:
// число
case STRING:
// строка
case WHITESPACE:
// whitespace
case PUNCTUATOR:
// символ пунктуации
...
}
Уменьшаем число сравнений
69ms → 14 ms
(еще в 5 раз быстрее!)
Предварительные итоги
Результат: 356ms → 14 ms
Цифры получены на упрощенном
примере, на практике всё сложнее
Примеры тестов:
gist.github.com/lahmatiy/e124293c2f2b98e847d0f2bb54c2738e
46
47
• charAt() → charCodeAt()
• Избегайте лишних операций со строками
• Контролируйте выход за пределы
• Уменьшайте количество сравнений
Подводим итоги
Токен – не только строка
Нужна дополнительная информация
о токене: тип и локация
49
50
.foo {
width: 10px;
}
[
{
type: 'FullStop',
value: '.',
offset: 0,
line: 1,
column: 1
},
…
]
Как хранить?
51
Object Array
Понятнее Компактнее
{
type: 'FullStop',
value: '.',
offset: 1,
line: 2,
column: 3
}
['FullStop', '.', 1, 2, 3]
Размер токена
52
Object Array
64 байт 88 байт
Размер токена
52
Object Array
64 байт 88 байт
Наш CSS – 885Kb
256 163 токена
16,4 Мб 22,5 Мб
разница 6,1 Мб
Что быстрее?
53
Object Array
Время создания (литералов) сопоставимо,

меняется в зависимости от задачи
Обработка объектов (чтение) гораздо быстрее
Нет простого ответа
gist.github.com/lahmatiy/90fa91d5ea89465d08c7db1e591e91c2
Меняем подход
Последовательные токенайзер и
лексер – проще,
но ведет к лишним затратам памяти
и медленней
55
Scanner
(ленивый токенайзер)
56
57
scanner.token // текущий токен или null
scanner.next() // переход к следующему токену
scanner.lookup(N) // заглядывание вперед, возвращает
// токен на N-ой позиции от текущей
Основное API
58
• lookup(N)

заполняет буфер токенов до позиции N, если еще
не заполнен, возвращает N-1 токен из буфера
• next()

делает shift из lookup буфера, если он не пустой,
либо читает новый токен
Создается столько же токенов, 

но нужно меньше памяти в один
момент времени
59
60
• Переиспользование объектов токенов
• Пул токенов фиксированного размера
Неудачные эксперименты
Выигрыш практически не ощущается, но код
гораздо сложнее и в разных случаях нужен пул
разного размера, иногда очень большой
Сборка дерева
62
• Избавление от look ahead
• Array → Object
• Array → List
• CST → AST
• …
Как ускорялся
Look ahead
64
function getSelector() {
var node = ['Selector'];
while (i < tokens.length) {
if (checkId()) {
node.push(getId());
} else if (checkClass()) {
node.push(getClass());
} else ...
}
return node;
}
Было
65
function getSelector() {
var node = ['Selector'];
while (i < tokens.length) {
if (checkId()) {
node.push(getId());
} else if (checkClass()) {
node.push(getClass());
} else ...
}
return node;
}
Было
Checker-функции –
проверяют, подходящая
ли последовательность
токенов чтобы собрать
AST узел
66
function getSelector() {
var node = ['Selector'];
while (i < tokens.length) {
if (checkId()) {
node.push(getId());
} else if (checkClass()) {
node.push(getClass());
} else ...
}
return node;
}
Было
Build-функции –
собирают AST узел, 

у них есть гарантия, 

что впереди подходящая
последовательность
токенов
67
• Многократный проход по списку токенов
• Нужны все токены сразу
• Большое дублирование кода (проверок)
• Сложно определить местоположение, в
случае синтаксической ошибки в CSS
Проблемы
68
StyleSheet
Ruleset
Selector
SimpleSelector
Id | Class | Attribute …
…
Block
…
…
Насколько все плохо
checkStyleSheet()
проверяла всю
последовательность
токенов, прежде чем
начиналась сборка AST
– Cascading Style Sheets Level 2 Specification
“The lexical scanner for the CSS core syntax
in section 4.1.1 can be implemented 

as a scanner without back-up.”
69
www.w3.org/TR/CSS22/grammar.html#q4
70
while (scanner.token !== null) {
switch (scanner.token.type) {
case TokenType.Hash: // #
node.push(getId());
break;
case TokenType.FullStop: // .
node.push(getClass());
break;
…
}
Стало
Быстрая проверка типа
токена. В большинстве
случае этого
достаточно, чтобы
выбрать подходящую
функцию сборки
71
function getPseudo() {
var next = scanner.lookup(1);
if (next.type === TokenType.Colon) {
return getPseudoElement();
}
return getPseudoClass();
}
Стало
Заглядывать вперед
нужно редко, в этих
случаях используется
scanner.lookup(N)
72
if (scanner.token.type !== TokenType.Something) {
throwError('Something wrong here');
}
Стало
Если встречается что-то "не то" – 

тут же выбрасывается сообщение об ошибке
73
• Значительно быстрее
• Используется ленивый токенайзер
• Код проще, мало дублирования
• Точное местоположение для ошибок в CSS
Сплошные плюсы
Array → Object
75
[ "stylesheet", [
"atrules",
[
"atkeyword",
[ "ident", "import" ]
],
[ "s", " " ],
[ "string", "'path/to/file.css'" ]
]
]
Было – массив массивов
76
{
"type": "StyleSheet",
"rules": [{
"type": "Atrule",
"name": "import",
"expression": {
"type": "AtruleExpression",
"sequence": [ ... ]
},
"block": null
}]
}
Стало
77
• Требуется меньше памяти
• Быстрее работа с узлами
• Проще писать/читать код 

(node[1] vs. node.value)
Результат
78
Помни – меньше мутаций!
function createNode() {
var node = {
type: 'Type'
};
if (expression1) {
node.foo = 123;
}
if (expression2) {
node.bar = true;
}
return node;
}
При добавлении нового
свойства создается новый
hidden class (здесь до 4).
Узел одного типа, 

но объекты разных классов –
принимающие функции
станут полиморфными.
79
Добавляется новое свойство
→ меняется hidden class
80
Смена hidden class
81
Фиксим
function createNode() {
var node = {
type: 'Type',
foo: null,
bar: false
};
if (expression1) {
node.foo = 123;
}
if (expression2) {
node.bar = true;
}
return node;
}
При создании объекта
стоит указывать все
возможные свойства –
будет один hidden class
82
Все стало проще
И функции работающие с объектами 

только этого типа будут мономорфными
CST → AST
84
• AST (Abstract Syntax Tree)

логическая структура
• CST (Concrete Syntax Tree)

логическая структура + информация о
форматировании
AST vs. CST
85
• AST – не нужно форматирование

linters, minifiers, beautifiers, …
• CST – форматирование важно

linters, autoprefixer, postcss и друзья
AST vs. CST
Раньше парсер возвращал CST,
то есть сохранял все 

пробелы, комментарии и разделители
86
87
.foo > .bar,
.baz {
border: 1px solid red; // I'm useless comment
}
Отмеченное желтым – не нужно, 

но сохранялось в дереве, а компрессор
это все удалял
Так же теперь парсер не собирает
заведомо неверные конструкции, 

что упрощает и ускоряет дальнейшую
работу с деревом 

(т.к. не требуются лишние проверки)
88
89
{
"type": "Dimension",
"value": {
"type": "Number",
"value": "123"
},
"unit": {
"type": "Identifier",
"name": "px"
}
}
Упрощен формат некоторых узлов
{
"type": "Dimension",
"value": "123",
"unit": "px"
}
90
• Меньше узлов в дереве
• Не нужен обход, чтобы удалить ненужное
• Многое проще не добавлять в дерево,
чем потом искать и удалять
Результат
91
• Наш CSS – 885Kb
• было узлов – 281 750
• стало узлов – 139 858
• Экономия памяти ~7Mb
• В два раза быстрее обход
В цифрах
Подводим итоги
93
• В ~5 раз быстрее
• В ~3 раза меньше потребление памяти
• Проще формат
• Дешевле обход и обработка AST
Улучшения парсера
Компрессор
95
• Однопроходная реструктуризация
• Уменьшение числа обходов
• Уменьшение глубины обхода
• Минимум вычислений, мутаций узлов, копирования
• Учет специфики CSS
• …
Как ускорялся
Однопроходная
реструктуризация
97
var prevAst = ast;
var prev = translate(ast);
while (true) {
var resAst = restruct(copy(prevAst));
var res = translate(resAst);
if (res.length > prev.length) {
resAst = prevAst;
break;
}
prevAst = resAst;
prev = res;
}
Трансформируем
пока есть
положительный
эффект
98
var prevAst = ast;
var prev = translate(ast);
while (true) {
var resAst = restruct(copy(prevAst));
var res = translate(resAst);
if (res.length > prev.length) {
resAst = prevAst;
break;
}
prevAst = resAst;
prev = res;
}
На каждой итерации
• копирование дерева
99
var prevAst = ast;
var prev = translate(ast);
while (true) {
var resAst = restruct(copy(prevAst));
var res = translate(resAst);
if (res.length > prev.length) {
resAst = prevAst;
break;
}
prevAst = resAst;
prev = res;
}
На каждой итерации
• копирование дерева
• трансляция в строку
100
var prevAst = ast;
var prev = translate(ast);
while (true) {
var resAst = restruct(copy(prevAst));
var res = translate(resAst);
if (res.length > prev.length) {
resAst = prevAst;
break;
}
prevAst = resAst;
prev = res;
}
На каждой итерации
• копирование дерева
• трансляция в строку
• бай-бай GC
101
var prevAst = ast;
var prev = translate(ast);
while (true) {
var resAst = restruct(copy(prevAst));
var res = translate(resAst);
if (res.length > prev.length) {
resAst = prevAst;
break;
}
prevAst = resAst;
prev = res;
}
На каждой итерации
• копирование дерева
• трансляция в строку
• бай-бай GC
😱
Стало
102
restruct(ast);
• Не ошибка – один проход
• Нет копирования AST, трансляции всего дерева в строку
• Пришлось поменять алгоритмы
• Меньше нагрузка на GC, быстрее в 3-4 раза
Глубина обхода
104
Наш CSS – 885Kb
139 858 узлов
Холостой обход дерева
31 ms
Многим трансформациям
необходимо обходить лишь
Ruleset и AtRule
105
106
• walkAll() – обход всех узлов
• walkRules() – обход только правил и at-rules
• walkRulesRight() – обход только правил 

и at-rules в обратном порядке
Walker под задачу
107
Наш CSS – 139 858 узлов
walkAll()
31 ms
Холостой обход дерева
walkRules(), walkRulesRight()
2 ms
обходится 6 154 узла (4%)
Меньше обходов
109
walkAll(ast, function(node) {
if (node.type === 'Number') {
…
}
});
walkAll(ast, function(node) {
if (node.type === 'String') {
…
}
});
…
Было
Для каждого типа узла
отдельный обход
Как мы помним, для
нашего дерева
операция стоит 31 ms
110
walkAll(ast, function(node) {
if (node.type === 'Number') {
…
}
});
walkAll(ast, function(node) {
if (node.type === 'String') {
…
}
});
…
Было
Функции получают все
узлы дерева, а они
разных hidden class.
Так функции будут
полиморфными, хотя
работают (обычно)
только с одним типом
узлов
111
var handlers = {
Number: packNumber,
String: packString,
…
};
walkAll(ast, function(node, item, list) {
if (handlers.hasOwnProperty(node.type)) {
handlers[node.type].call(this, node, item, list);
}
});
Стало
Один проход
112
var handlers = {
Number: packNumber,
String: packString,
…
};
walkAll(ast, function(node, item, list) {
if (handlers.hasOwnProperty(node.type)) {
handlers[node.type].call(this, node, item, list);
}
});
Стало
Единственная
полиморфная функция,
но простая
113
var handlers = {
Number: packNumber,
String: packString,
…
};
walkAll(ast, function(node, item, list) {
if (handlers.hasOwnProperty(node.type)) {
handlers[node.type].call(this, node, item, list);
}
});
Стало
Функции оказываются
мономорфными
(им приходят объекты
одного hidden class)
114
var handlers = {
Number: packNumber,
String: packString,
…
};
walkAll(ast, function(node, item, list) {
if (handlers.hasOwnProperty(node.type)) {
handlers[node.type].call(this, node, item, list);
}
});
Стало
По результатам
экспериментов, наиболее
быстрый вариант решения
Общий итог
116
• В 10+ раз быстрее
• В 7+ раз меньше потребление памяти
• В настоящий момент быстрее и эффективнее 

по ресурсам других структурных минификаторов 

(cssnano, clean-css)
Текущие результаты
goalsmashers.github.io/css-minification-benchmark/
Работы не закончены,
можно сделать еще лучше ;)
117
CSSO — сжимаем CSS
118
www.slideshare.net/basisjs/csso-css-2
Немного о принципах минификации, 

новых продвинутых оптимизациях и открытых вопросах
Бонус трек
Обкладывайте операции замерами
120
> echo '.test { color: green; }' | csso --debug
## parsing done in 11 ms
Compress block #1
[0.000s] init
[0.001s] clean
[0.001s] compress
[0.004s] prepare
[0.001s] initialMergeRuleset
[0.000s] mergeAtrule
[0.000s] disjoinRuleset
[0.001s] restructShorthand
[0.003s] restructBlock
...
Hi-res timing
121
var startTime = process.hrtime();
// your code
var elapsed = process.hrtime(startTime);
console.log(parseInt(elapsed[0] * 1e3 + elapsed[1] / 1e6));
var startTime = performance.now();
// your code
console.log(performance.now() - startTime);
Node.js
Browser
Memory usage
122
var mem = process.memoryUsage().heapUsed;
// your code
console.log(process.memoryUsage().heapUsed - mem);
Очень грубый метод,
лучшего простого способа пока нет :'(
Привычные инструменты
devtool
124
Runs Node.js programs inside Chrome DevTools
npm install -g devtool
github.com/Jam3/devtool
> devtool your-script.js
125
Заглядывайте под капот
127
node --trace-hydrogen 
--trace-phase=Z 
--trace-deopt 
--code-comments 
--hydrogen-track-positions 
--redirect-code-traces 
--redirect-code-traces-to=code.asm 
--trace_hydrogen_file=code.cfg 
--print-opt-code 
your-script.js
Получаем данные о работе кода
128
mrale.ph/irhydra/2/
code.asm
code.cfg
Самое сложное научиться это
понимать ;)
130
Заключение
Вам это все не нужно,
но однажды может пригодиться
132
133
• Подбирать решение под задачу
• Меньше тратить памяти
• Меньше мутаций
• Проще структуры
• Разбираться как выполняется код
• Перестать думать, что вы умней VM и наоборот ;)
Капитанские советы
Нет серебряной пули –
экспериментируйте, заменяйте, 

смотрите под капот
134
Изучайте алгоритмы и структуры данных –
правильный их выбор может дать больше
чем оптимизация кода
135
Роман Дворнов
@rdvornov
rdvornov@gmail.com
Вопросы?
github.com/css/csso

CSSO – история ускорения

  • 1.
    CSSO — историяускорения Роман Дворнов Avito HolyJS Санкт-Петербург, 2016
  • 2.
    Работаю в Avito ДелаюSPA Автор basis.js Мейнтенер CSSO За любую движуху, 
 кроме голодовки ;)
  • 3.
    ВремясжатияCSS(600Kb) 500 ms 1 000 ms 1 500ms 2 000 ms 2 500 ms 3 000 ms 3 500 ms 4 000 ms 4 500 ms 5 000 ms 5 500 ms 6 000 ms Версия CSSO 1.4.0 1.5.0 1.6.0 1.7.0 1.8.0 2.0 1 050 ms clean-css Изменение скорости CSSO csso 500 ms cssnano 23 250 ms
  • 4.
  • 5.
    5 clean-css 3.4.16 cssnano3.6.2 csso 2.1.1 ноябрь 2015 602 245 байт 430 240 2 039 ms 439 210 41 355 ms 435 629 714 ms апрель 2016 822 021 байт 587 906 2 546 ms 604 167 76 665 ms 595 834 793 ms июнь 2016 883 498 байт 632 007 2 588 ms 648 579 94 867 ms 639 495 803 ms Время минификации CSS Основной CSS файл ActiAgent.ru
  • 6.
    Тренд изменения времениот размера CSSТрендизмененийразмераивремени 100 % 120 % 140 % 160 % 180 % 200 % 220 % 240 % Размер CSS 602 245 822 021 883 498 размер CSS clean-css cssnano csso 229% 146% 127% 112%
  • 7.
    7 Новые продвинутые оптимизации (usagedata, rename, etc) 245 823 Время Размер (байт) 639 495 803 ms 2 513 ms Лучше сжатие, но требует 2-3х больше времени
  • 8.
    8 clean-css 3.4.16 cssnano3.6.2 csso 2.1.1 15,5 sec 9 min 29 sec 4,8 sec Таких файлов в проекте несколько (6 больших) время минификации всех файлов
  • 9.
    8 clean-css 3.4.16 cssnano3.6.2 csso 2.1.1 15,5 sec 9 min 29 sec 4,8 sec Таких файлов в проекте несколько (6 больших) время минификации всех файлов clean-css 3.4.16 cssnano 3.6.2 csso 2.1.1 не поддерживается (≈46,5 sec) не поддерживается (≈28 min 27 sec) ~15 sec + продвинутые оптимизации
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
    Основные шаги • Токенизация •Построение дерева (лексер) 11
  • 19.
  • 20.
    13 • whitespaces –[ nrtf]+ • keyword – [a-zA-aZ…]+ • number – [0-9]+ • string – "string" или 'string' • comment – /* comment */ • punctuation – [;,.#{}[]()…] Разбиение текста на токены
  • 21.
    14 .foo { width: 10px; } [ '.','foo', ' ', '{', 'n ', 'width', ':', ' ', '10', 'px', ';', 'n', '}' ]
  • 22.
  • 23.
    16 for (var i= 0; i < str.length; i++) { var ch = str.charAt(i); var chNext = str.charAt(i + 1); if (ch === '/' && chNext === '*') { result.push(readComment()); } else if (ch >= '0' && ch <= '9') { result.push(readNumber()); } ... } Ключевой цикл токенайзера
  • 24.
    17 for (var i= 0; i < str.length; i++) { var ch = str.charAt(i); var chNext = str.charAt(i + 1); if (ch === '/' && chNext === '*') { result.push(readComment()); } else if (ch >= '0' && ch <= '9') { result.push(readNumber()); } ... } Ключевой цикл токенайзера
  • 25.
    18 for (var i= 0; i < str.length; i++) { var ch = str.charAt(i); var chNext = str.charAt(i + 1); if (ch === '/' && chNext === '*') { result.push(readComment()); } else if (ch >= '0' && ch <= '9') { result.push(readNumber()); } ... } Ключевой цикл токенайзера
  • 26.
    19 for (var i= 0; i < str.length; i++) { var ch = str.charAt(i); var chNext = str.charAt(i + 1); if (ch === '/' && chNext === '*') { result.push(readComment()); } else if (ch >= '0' && ch <= '9') { result.push(readNumber()); } ... } Ключевой цикл токенайзера
  • 27.
    20 for (var i= 0; i < str.length; i++) { var ch = str.charAt(i); var chNext = str.charAt(i + 1); if (ch === '/' && chNext === '*') { result.push(readComment()); } else if (ch >= '0' && ch <= '9') { result.push(readNumber()); } ... } Ключевой цикл токенайзера
  • 28.
    Обычный код –простор для потери быстродействия 21
  • 29.
  • 30.
    23 for (var i= 0; i < str.length; i++) { var ch = str.charAt(i); var chNext = str.charAt(i + 1); if (ch === '/' && chNext === '*') { result.push(readComment()); } else if (ch >= '0' && ch <= '9') { result.push(readNumber()); } ... } Сравнение символов
  • 31.
    24 • Строка – последовательностьчисел • Извлечение символа = получение новой строки • Сравнение символов ≈ сравнение строк Нужно помнить
  • 32.
  • 33.
    26 // выносим кодысимволов в константы var STAR = 42; var SLASH = 47; var ZERO = 48; var NINE = 57; ... Делаем быстрее
  • 34.
    27 for (var i= 0; i < str.length; i++) { var code = str.charCodeAt(i); var codeNext = str.charCodeAt(i + 1); if (code === SLASH && codeNext === STAR) { result.push(readComment()); } else if (code >= ZERO && code <= NINE) { result.push(readNumber()); } ... } charCodeAt() + числовые константы
  • 35.
  • 36.
  • 37.
    Используй const, Люк! 29 varSTAR = 42; var SLASH = 47; var ZERO = 48; var NINE = 57; ... const STAR = 42; const SLASH = 47; const ZERO = 48; const NINE = 57; ...
  • 38.
  • 39.
    – Вячеслав Егоров “...[const] это все-таки неизменяемая привязка переменной к значению ... С другой стороны виртуальная машина может и должна бы использовать это самое свойство неизменяемости ... V8 не использует, к сожалению.” 31 habrahabr.ru/company/jugru/blog/301040/#comment_9622474
  • 40.
    32 for (var i= 0; i < str.length; i++) { var code = str.charCodeAt(i); var codeNext = str.charCodeAt(i + 1); if (code === 47 && codeNext === 42) { result.push(readComment()); } else if (code >= 48 && code <= 57) { result.push(readNumber()); } ... } Но мы люди упоротые упорные
  • 41.
    32 for (var i= 0; i < str.length; i++) { var code = str.charCodeAt(i); var codeNext = str.charCodeAt(i + 1); if (code === 47 && codeNext === 42) { result.push(readComment()); } else if (code >= 48 && code <= 57) { result.push(readNumber()); } ... } Но мы люди упоротые упорные Положительный эффект не замечен…
  • 42.
    33 356ms – chatAt() 307ms– charCodeAt() Результаты на ~14% быстрее
  • 43.
  • 44.
    35 for (var i= 0; i < str.length; i++) { var code = str.charCodeAt(i); var codeNext = str.charCodeAt(i + 1); if (code === SLASH && codeNext === STAR) { result.push(readComment()); } else if (code >= ZERO && code <= NINE) { result.push(readNumber()); } ... } Избегаем лишних чтений из строки
  • 45.
    36 for (var i= 0; i < str.length; i++) { var code = str.charCodeAt(i); if (code === SLASH) { var codeNext = str.charCodeAt(i + 1); if (codeNext === STAR) { result.push(readComment()); continue; } } ... } Избегаем лишних чтений из строки 307ms → 292 ms (-5%)
  • 46.
    37 for (var i= 0; i < str.length; i++) { var code = str.charCodeAt(i); if (code === SLASH) { var codeNext = str.charCodeAt(i + 1); if (codeNext === STAR) { result.push(readComment()); continue; } } ... } Опасная операция!
  • 47.
    38 Попытка чтения запределами 
 строки или массива 
 приводит к деоптимизации
  • 48.
    39 var codeNext =0; if (i + 1 < str.length) { codeNext = str.charCodeAt(i + 1); } Избегаем выхода за пределы строки 292ms → 69 ms (в 4 раза быстрее!)
  • 49.
  • 50.
    41 if (code >=ZERO && code <= NINE) { // число } else if (code === SINGLE_QUOTE || code === DOUBLE_QUOTE) { // строка } else if (code === SPACE || code === N || code === R || code === F) { // whitespace } … Много сравнений
  • 51.
    42 var PUNCTUATION ={ 9: 'Tab', // 't' 10: 'Newline', // 'n' ... 126: 'Tilde' // '~' }; ... } else if (code in PUNCTUATION) { // символ пунктуации } ... Проверка в словаре – очень дорого
  • 52.
    43 var CATEGORY_LENGTH =Math.max.apply(null, Object.keys(PUNCTUATION)) + 1; var CATEGORY = new Uint32Array(CATEGORY_LENGTH); Object.keys(PUNCTUATION).forEach(function(key) { CATEGORY[key] = PUNCTUATOR; }); for (var i = ZERO; i <= NINE; i++) { CATEGORY[i] = DIGIT; } CATEGORY[SINGLE_QUOTE] = STRING; CATEGORY[DOUBLE_QUOTE] = STRING; CATEGORY[SPACE] = WHITESPACE; ... Создаем карту категорий
  • 53.
    44 switch (code <CATEGORY_LENGTH ? CATEGORY[code] : 0) { case DIGIT: // число case STRING: // строка case WHITESPACE: // whitespace case PUNCTUATOR: // символ пунктуации ... } Уменьшаем число сравнений 69ms → 14 ms (еще в 5 раз быстрее!)
  • 54.
  • 55.
    Результат: 356ms →14 ms Цифры получены на упрощенном примере, на практике всё сложнее Примеры тестов: gist.github.com/lahmatiy/e124293c2f2b98e847d0f2bb54c2738e 46
  • 56.
    47 • charAt() →charCodeAt() • Избегайте лишних операций со строками • Контролируйте выход за пределы • Уменьшайте количество сравнений Подводим итоги
  • 57.
    Токен – нетолько строка
  • 58.
    Нужна дополнительная информация отокене: тип и локация 49
  • 59.
    50 .foo { width: 10px; } [ { type:'FullStop', value: '.', offset: 0, line: 1, column: 1 }, … ]
  • 60.
    Как хранить? 51 Object Array ПонятнееКомпактнее { type: 'FullStop', value: '.', offset: 1, line: 2, column: 3 } ['FullStop', '.', 1, 2, 3]
  • 61.
  • 62.
    Размер токена 52 Object Array 64байт 88 байт Наш CSS – 885Kb 256 163 токена 16,4 Мб 22,5 Мб разница 6,1 Мб
  • 63.
    Что быстрее? 53 Object Array Времясоздания (литералов) сопоставимо,
 меняется в зависимости от задачи Обработка объектов (чтение) гораздо быстрее Нет простого ответа gist.github.com/lahmatiy/90fa91d5ea89465d08c7db1e591e91c2
  • 64.
  • 65.
    Последовательные токенайзер и лексер– проще, но ведет к лишним затратам памяти и медленней 55
  • 66.
  • 67.
    57 scanner.token // текущийтокен или null scanner.next() // переход к следующему токену scanner.lookup(N) // заглядывание вперед, возвращает // токен на N-ой позиции от текущей Основное API
  • 68.
    58 • lookup(N)
 заполняет буфертокенов до позиции N, если еще не заполнен, возвращает N-1 токен из буфера • next()
 делает shift из lookup буфера, если он не пустой, либо читает новый токен
  • 69.
    Создается столько жетокенов, 
 но нужно меньше памяти в один момент времени 59
  • 70.
    60 • Переиспользование объектовтокенов • Пул токенов фиксированного размера Неудачные эксперименты Выигрыш практически не ощущается, но код гораздо сложнее и в разных случаях нужен пул разного размера, иногда очень большой
  • 71.
  • 72.
    62 • Избавление отlook ahead • Array → Object • Array → List • CST → AST • … Как ускорялся
  • 73.
  • 74.
    64 function getSelector() { varnode = ['Selector']; while (i < tokens.length) { if (checkId()) { node.push(getId()); } else if (checkClass()) { node.push(getClass()); } else ... } return node; } Было
  • 75.
    65 function getSelector() { varnode = ['Selector']; while (i < tokens.length) { if (checkId()) { node.push(getId()); } else if (checkClass()) { node.push(getClass()); } else ... } return node; } Было Checker-функции – проверяют, подходящая ли последовательность токенов чтобы собрать AST узел
  • 76.
    66 function getSelector() { varnode = ['Selector']; while (i < tokens.length) { if (checkId()) { node.push(getId()); } else if (checkClass()) { node.push(getClass()); } else ... } return node; } Было Build-функции – собирают AST узел, 
 у них есть гарантия, 
 что впереди подходящая последовательность токенов
  • 77.
    67 • Многократный проходпо списку токенов • Нужны все токены сразу • Большое дублирование кода (проверок) • Сложно определить местоположение, в случае синтаксической ошибки в CSS Проблемы
  • 78.
    68 StyleSheet Ruleset Selector SimpleSelector Id | Class| Attribute … … Block … … Насколько все плохо checkStyleSheet() проверяла всю последовательность токенов, прежде чем начиналась сборка AST
  • 79.
    – Cascading StyleSheets Level 2 Specification “The lexical scanner for the CSS core syntax in section 4.1.1 can be implemented 
 as a scanner without back-up.” 69 www.w3.org/TR/CSS22/grammar.html#q4
  • 80.
    70 while (scanner.token !==null) { switch (scanner.token.type) { case TokenType.Hash: // # node.push(getId()); break; case TokenType.FullStop: // . node.push(getClass()); break; … } Стало Быстрая проверка типа токена. В большинстве случае этого достаточно, чтобы выбрать подходящую функцию сборки
  • 81.
    71 function getPseudo() { varnext = scanner.lookup(1); if (next.type === TokenType.Colon) { return getPseudoElement(); } return getPseudoClass(); } Стало Заглядывать вперед нужно редко, в этих случаях используется scanner.lookup(N)
  • 82.
    72 if (scanner.token.type !==TokenType.Something) { throwError('Something wrong here'); } Стало Если встречается что-то "не то" – 
 тут же выбрасывается сообщение об ошибке
  • 83.
    73 • Значительно быстрее •Используется ленивый токенайзер • Код проще, мало дублирования • Точное местоположение для ошибок в CSS Сплошные плюсы
  • 84.
  • 85.
    75 [ "stylesheet", [ "atrules", [ "atkeyword", ["ident", "import" ] ], [ "s", " " ], [ "string", "'path/to/file.css'" ] ] ] Было – массив массивов
  • 86.
    76 { "type": "StyleSheet", "rules": [{ "type":"Atrule", "name": "import", "expression": { "type": "AtruleExpression", "sequence": [ ... ] }, "block": null }] } Стало
  • 87.
    77 • Требуется меньшепамяти • Быстрее работа с узлами • Проще писать/читать код 
 (node[1] vs. node.value) Результат
  • 88.
    78 Помни – меньшемутаций! function createNode() { var node = { type: 'Type' }; if (expression1) { node.foo = 123; } if (expression2) { node.bar = true; } return node; } При добавлении нового свойства создается новый hidden class (здесь до 4). Узел одного типа, 
 но объекты разных классов – принимающие функции станут полиморфными.
  • 89.
  • 90.
  • 91.
    81 Фиксим function createNode() { varnode = { type: 'Type', foo: null, bar: false }; if (expression1) { node.foo = 123; } if (expression2) { node.bar = true; } return node; } При создании объекта стоит указывать все возможные свойства – будет один hidden class
  • 92.
    82 Все стало проще Ифункции работающие с объектами 
 только этого типа будут мономорфными
  • 93.
  • 94.
    84 • AST (AbstractSyntax Tree)
 логическая структура • CST (Concrete Syntax Tree)
 логическая структура + информация о форматировании AST vs. CST
  • 95.
    85 • AST –не нужно форматирование
 linters, minifiers, beautifiers, … • CST – форматирование важно
 linters, autoprefixer, postcss и друзья AST vs. CST
  • 96.
    Раньше парсер возвращалCST, то есть сохранял все 
 пробелы, комментарии и разделители 86
  • 97.
    87 .foo > .bar, .baz{ border: 1px solid red; // I'm useless comment } Отмеченное желтым – не нужно, 
 но сохранялось в дереве, а компрессор это все удалял
  • 98.
    Так же теперьпарсер не собирает заведомо неверные конструкции, 
 что упрощает и ускоряет дальнейшую работу с деревом 
 (т.к. не требуются лишние проверки) 88
  • 99.
    89 { "type": "Dimension", "value": { "type":"Number", "value": "123" }, "unit": { "type": "Identifier", "name": "px" } } Упрощен формат некоторых узлов { "type": "Dimension", "value": "123", "unit": "px" }
  • 100.
    90 • Меньше узловв дереве • Не нужен обход, чтобы удалить ненужное • Многое проще не добавлять в дерево, чем потом искать и удалять Результат
  • 101.
    91 • Наш CSS– 885Kb • было узлов – 281 750 • стало узлов – 139 858 • Экономия памяти ~7Mb • В два раза быстрее обход В цифрах
  • 102.
  • 103.
    93 • В ~5раз быстрее • В ~3 раза меньше потребление памяти • Проще формат • Дешевле обход и обработка AST Улучшения парсера
  • 104.
  • 105.
    95 • Однопроходная реструктуризация •Уменьшение числа обходов • Уменьшение глубины обхода • Минимум вычислений, мутаций узлов, копирования • Учет специфики CSS • … Как ускорялся
  • 106.
  • 107.
    97 var prevAst =ast; var prev = translate(ast); while (true) { var resAst = restruct(copy(prevAst)); var res = translate(resAst); if (res.length > prev.length) { resAst = prevAst; break; } prevAst = resAst; prev = res; } Трансформируем пока есть положительный эффект
  • 108.
    98 var prevAst =ast; var prev = translate(ast); while (true) { var resAst = restruct(copy(prevAst)); var res = translate(resAst); if (res.length > prev.length) { resAst = prevAst; break; } prevAst = resAst; prev = res; } На каждой итерации • копирование дерева
  • 109.
    99 var prevAst =ast; var prev = translate(ast); while (true) { var resAst = restruct(copy(prevAst)); var res = translate(resAst); if (res.length > prev.length) { resAst = prevAst; break; } prevAst = resAst; prev = res; } На каждой итерации • копирование дерева • трансляция в строку
  • 110.
    100 var prevAst =ast; var prev = translate(ast); while (true) { var resAst = restruct(copy(prevAst)); var res = translate(resAst); if (res.length > prev.length) { resAst = prevAst; break; } prevAst = resAst; prev = res; } На каждой итерации • копирование дерева • трансляция в строку • бай-бай GC
  • 111.
    101 var prevAst =ast; var prev = translate(ast); while (true) { var resAst = restruct(copy(prevAst)); var res = translate(resAst); if (res.length > prev.length) { resAst = prevAst; break; } prevAst = resAst; prev = res; } На каждой итерации • копирование дерева • трансляция в строку • бай-бай GC 😱
  • 112.
    Стало 102 restruct(ast); • Не ошибка– один проход • Нет копирования AST, трансляции всего дерева в строку • Пришлось поменять алгоритмы • Меньше нагрузка на GC, быстрее в 3-4 раза
  • 113.
  • 114.
    104 Наш CSS –885Kb 139 858 узлов Холостой обход дерева 31 ms
  • 115.
  • 116.
    106 • walkAll() –обход всех узлов • walkRules() – обход только правил и at-rules • walkRulesRight() – обход только правил 
 и at-rules в обратном порядке Walker под задачу
  • 117.
    107 Наш CSS –139 858 узлов walkAll() 31 ms Холостой обход дерева walkRules(), walkRulesRight() 2 ms обходится 6 154 узла (4%)
  • 118.
  • 119.
    109 walkAll(ast, function(node) { if(node.type === 'Number') { … } }); walkAll(ast, function(node) { if (node.type === 'String') { … } }); … Было Для каждого типа узла отдельный обход Как мы помним, для нашего дерева операция стоит 31 ms
  • 120.
    110 walkAll(ast, function(node) { if(node.type === 'Number') { … } }); walkAll(ast, function(node) { if (node.type === 'String') { … } }); … Было Функции получают все узлы дерева, а они разных hidden class. Так функции будут полиморфными, хотя работают (обычно) только с одним типом узлов
  • 121.
    111 var handlers ={ Number: packNumber, String: packString, … }; walkAll(ast, function(node, item, list) { if (handlers.hasOwnProperty(node.type)) { handlers[node.type].call(this, node, item, list); } }); Стало Один проход
  • 122.
    112 var handlers ={ Number: packNumber, String: packString, … }; walkAll(ast, function(node, item, list) { if (handlers.hasOwnProperty(node.type)) { handlers[node.type].call(this, node, item, list); } }); Стало Единственная полиморфная функция, но простая
  • 123.
    113 var handlers ={ Number: packNumber, String: packString, … }; walkAll(ast, function(node, item, list) { if (handlers.hasOwnProperty(node.type)) { handlers[node.type].call(this, node, item, list); } }); Стало Функции оказываются мономорфными (им приходят объекты одного hidden class)
  • 124.
    114 var handlers ={ Number: packNumber, String: packString, … }; walkAll(ast, function(node, item, list) { if (handlers.hasOwnProperty(node.type)) { handlers[node.type].call(this, node, item, list); } }); Стало По результатам экспериментов, наиболее быстрый вариант решения
  • 125.
  • 126.
    116 • В 10+раз быстрее • В 7+ раз меньше потребление памяти • В настоящий момент быстрее и эффективнее 
 по ресурсам других структурных минификаторов 
 (cssnano, clean-css) Текущие результаты goalsmashers.github.io/css-minification-benchmark/
  • 127.
    Работы не закончены, можносделать еще лучше ;) 117
  • 128.
    CSSO — сжимаемCSS 118 www.slideshare.net/basisjs/csso-css-2 Немного о принципах минификации, 
 новых продвинутых оптимизациях и открытых вопросах
  • 129.
  • 130.
    Обкладывайте операции замерами 120 >echo '.test { color: green; }' | csso --debug ## parsing done in 11 ms Compress block #1 [0.000s] init [0.001s] clean [0.001s] compress [0.004s] prepare [0.001s] initialMergeRuleset [0.000s] mergeAtrule [0.000s] disjoinRuleset [0.001s] restructShorthand [0.003s] restructBlock ...
  • 131.
    Hi-res timing 121 var startTime= process.hrtime(); // your code var elapsed = process.hrtime(startTime); console.log(parseInt(elapsed[0] * 1e3 + elapsed[1] / 1e6)); var startTime = performance.now(); // your code console.log(performance.now() - startTime); Node.js Browser
  • 132.
    Memory usage 122 var mem= process.memoryUsage().heapUsed; // your code console.log(process.memoryUsage().heapUsed - mem); Очень грубый метод, лучшего простого способа пока нет :'(
  • 133.
  • 134.
    devtool 124 Runs Node.js programsinside Chrome DevTools npm install -g devtool github.com/Jam3/devtool
  • 135.
  • 136.
  • 137.
    127 node --trace-hydrogen --trace-phase=Z --trace-deopt --code-comments --hydrogen-track-positions --redirect-code-traces --redirect-code-traces-to=code.asm --trace_hydrogen_file=code.cfg --print-opt-code your-script.js Получаем данные о работе кода
  • 138.
  • 140.
    Самое сложное научитьсяэто понимать ;) 130
  • 141.
  • 142.
    Вам это всене нужно, но однажды может пригодиться 132
  • 143.
    133 • Подбирать решениепод задачу • Меньше тратить памяти • Меньше мутаций • Проще структуры • Разбираться как выполняется код • Перестать думать, что вы умней VM и наоборот ;) Капитанские советы
  • 144.
    Нет серебряной пули– экспериментируйте, заменяйте, 
 смотрите под капот 134
  • 145.
    Изучайте алгоритмы иструктуры данных – правильный их выбор может дать больше чем оптимизация кода 135
  • 146.