Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

0

Share

Download to read offline

Парсим CSS

Download to read offline

«Парсим CSS», Роман Дворнов (Avito)

В ходе работы над CSSO мне пришлось погрузиться в процесс парсинга CSS. В результате парсер (тот, что в CSSO) был не раз переписан. Пришло время сделать его отдельным инструментом. Новый быстрый детальный парсер CSS, его AST, области применения и кое-что ещё.

Related Books

Free with a 30 day trial from Scribd

See all
  • Be the first to like this

Парсим CSS

  1. 1. Парсим CSS performance tips & tricks Роман Дворнов Avito Москва, сентябрь 2016
  2. 2. Руководитель 
 фронтенда в Avito Основной интерес – SPA Open source:
 basis.js, CSSO, 
 component-inspector, 
 csstree и другие За любую движуху, 
 кроме голодовки ;)
  3. 3. Парсим CSS (зачем? почему? как дальше жить?) 3 tinyurl.com/csstree-intro Начало истории (доклад)
  4. 4. CSSTree
  5. 5. CSSTree – самый быстрый 
 и детальный парсер CSS 5
  6. 6. Как я до этого докатился?
  7. 7. Чуть меньше года назад 
 я стал мейнтейнером CSSO (минификатор CSS) 7 github.com/css/csso
  8. 8. CSSO работал на основе парсера Gonzales 8 github.com/css/gonzales
  9. 9. Проблемы • Не развивается с 2013 • Неудобный формат AST, местами странный • Много ошибок • Запутанная и сложная кодовая база • Медленный, потребляет много памяти, GC 9
  10. 10. Парсер – последнее, что я собирался трогать… 10
  11. 11. Альтернатива?
  12. 12. Парсеров CSS на JavaScript достаточно много 12
  13. 13. Частые проблемы • Заброшены и не развиваются • Устарели (не поддерживают новое в CSS) • Содержат ошибки • Неудачная структура • Медленные 13
  14. 14. Наилучшим выбором может быть парсер из PostCSS 14 postcss.org
  15. 15. Плюсы PostCSS • Развивается и поддерживается • Хорошо справляется с синтаксисом CSS и даже будущим + tolerant mode • Сохраняет информацию о форматировании • Удобное API для работы с AST • Быстрый 15
  16. 16. Основная проблема: селекторы и значения свойств остаются не разобранными (хранятся в виде строки) 16
  17. 17. Это вынуждает разработчиков • Использовать костыли • Писать свои парсеры • Использовать дополнительные парсеры:
 postcss-selector-parser
 postcss-value-parser 17
  18. 18. Переход на PostCSS означал написание собственных парсеров селекторов и свойств, что не сильно отличается от написания парсера целиком 18
  19. 19. Регулярный рефакторинг приводит к тому, что парсер может быть полностью переписан 
 (это норма 😳) 19
  20. 20. Парсер выделен в отдельный проект github.com/csstree/csstree 20
  21. 21. Скорость
  22. 22. CSSO – история ускорения (в том числе про парсер) 22 tinyurl.com/csso-speedup В предыдущих сериях (доклад)
  23. 23. После выступления разогнал парсер еще :) 23 * Вдохновленный общением с Вячеславом @mraleph Егоровым
  24. 24. 24 CSSTree: 24 ms Mensch: 31 ms CSSOM: 36 ms PostCSS: 38 ms Rework: 81 ms PostCSS Full: 100 ms Gonzales: 175 ms Stylecow: 176 ms Gonzales PE: 214 ms ParserLib: 414 ms bootstrap.css v3.3.7 (146Kb) github.com/postcss/benchmark Не детальное AST Детальное AST PostCSS Full = + postcss-selector-parser + postcss-value-parser
  25. 25. Epic fail как выяснилось позже, я вынес не ту версию парсера 25 😱 github.com/csstree/csstree/commit/57568c758195153e337f6154874c3bc42dd04450
  26. 26. 26 CSSTree: 24 ms Mensch: 31 ms CSSOM: 36 ms PostCSS: 38 ms Rework: 81 ms PostCSS Full: 100 ms Gonzales: 175 ms Stylecow: 176 ms Gonzales PE: 214 ms ParserLib: 414 ms bootstrap.css v3.3.7 (146Kb) github.com/postcss/benchmark На FrontTalks был показан результат до разгона 13 ms
  27. 27. Парсеры: курс молодого бойца
  28. 28. Основные шаги • Токенизация • Построение дерева (лексер) 28
  29. 29. Токенизация
  30. 30. 30 • whitespaces – [ nrtf]+ • keyword – [a-zA-aZ…]+ • number – [0-9]+ • string – "string" или 'string' • comment – /* comment */ • punctuation – [;,.#{}[]()…] Разбиение текста на токены
  31. 31. 31 .foo { width: 10px; } [ '.', 'foo', ' ', '{', 'n ', 'width', ':', ' ', '10', 'px', ';', 'n', '}' ]
  32. 32. Нужна дополнительная информация о токене: тип и локация 32 На этапе токенизации мы знаем тип и позицию, считать их после – дорого
  33. 33. 33 .foo { width: 10px; } [ { type: 'FullStop', value: '.', offset: 0, line: 1, column: 1 }, … ]
  34. 34. Сборка
  35. 35. 35 function getSelector() { var selector = { type: 'Selector', sequence: [] }; // main loop return selector; } Сборка
  36. 36. 36 for (;currentToken < tokenCount; currentToken++) { switch (tokens[currentToken]) { case TokenType.Hash: // # selector.sequence.push(getId()); break; case TokenType.FullStop: // . selector.sequence.push(getClass()); break; … } Main loop
  37. 37. 37 { "type": "StyleSheet", "rules": [{ "type": "Atrule", "name": "import", "expression": { "type": "AtruleExpression", "sequence": [ ... ] }, "block": null }] } Результат
  38. 38. История ускорения #2
  39. 39. 39 [ { type: 'FullStop', value: '.', offset: 0, line: 1, column: 1 }, … ] Стоимость токена: 24 + 5 * 4 + массив = min 50 bytes per token В нашем проекте ~1Mb CSS 254 062 токена = min 12.7 Mb
  40. 40. Прелюдия: меняем подход
  41. 41. Посчитать все токены, а потом 
 из них собирать AST – проще, но ведет к лишним затратам памяти и медленней 41
  42. 42. Scanner (ленивый токенайзер) 42
  43. 43. 43 scanner.token // текущий токен или null scanner.next() // переход к следующему токену scanner.lookup(N) // заглядывание вперед, возвращает // токен на N-ой позиции от текущей Основное API
  44. 44. 44 • lookup(N)
 заполняет буфер токенов до позиции N, если еще не заполнен, возвращает N-1 токен из буфера • next()
 делает shift из lookup буфера, если он не пустой, либо читает новый токен
  45. 45. Создается столько же токенов, 
 но нужно меньше памяти в один момент времени 45
  46. 46. Проблема: заставляем CG плакать работать 46
  47. 47. Уменьшаем стоимость токенов: «многоходовочка»
  48. 48. 48 [ { type: 'FullStop', value: '.', offset: 0, line: 1, column: 1 }, … ] Строковые обозначения удобны при отладке, но они не выходят за рамки сканера и можно заменить на числа
  49. 49. 49 [ { type: FULLSTOP, value: '.', offset: 0, line: 1, column: 1 }, … ] … // '.'.charCodeAt(0) var FULLSTOP = 46; …
  50. 50. 50 [ { type: 46, value: '.', offset: 0, line: 1, column: 1 }, … ]
  51. 51. 51 [ { type: 46, value: '.', offset: 0, line: 1, column: 1 }, … ] Можно не хранить подстроку – это особенно расточительно для одиночных символов; к тому же многие многие конструкции собираются из нескольких токенов – эффективнее брать одну подстроку вместо конкатенации нескольких
  52. 52. 52 [ { type: 46, value: '.', offset: 0, line: 1, column: 1 }, … ] [ { type: 46, start: 0, end: 1, line: 1, column: 1 }, … ]
  53. 53. 53 [ { type: 46, start: 0, end: 1, line: 1, column: 1 }, … ] Look, Ma! No strings just numbers!
  54. 54. 54 Да не просто Array, а TypedArray Массив 
 объектов Массивы 
 чисел
  55. 55. Array vs. TypedArray • Не могут содержать дырок • В теории быстрее (т.к. меньше проверок) • Хранятся вне heap (если достаточно большие) • Предзаполнены нулями 55
  56. 56. 56 [ { type: 46, start: 0, end: 1, line: 1, column: 1 }, … ] Uint8Array Uint32Array Uint32Array Uint32Array Uint32Array 1 4 4 4 4 17 per token (кол-во токенов) 254 062 x 17 = 4.3Mb
  57. 57. 4.3Mb vs. 12.7Mb(min) 57
  58. 58. Хьюстон, у нас проблемы: TypedArray фиксированной длины,
 а мы не знаем сколько токенов будет 58
  59. 59. 59 [ { type: 46, start: 0, end: 1, line: 1, column: 1 }, … ] Uint8Array Uint32Array Uint32Array Uint32Array Uint32Array 1 4 4 4 4 17 per token (кол-во символов) 983 085 x 17 = 16.7Mb
  60. 60. 16.7Mb vs. 12.7Mb (min) 60
  61. 61. 16.7Mb vs. 12.7Mb (min) 60 Не повод сдаваться, 
 давайте немного подумаем…
  62. 62. 61 start = [ 0, 5, 6, 7, 9, 11, …, 35 ] end = [ 5, 6, 7, 9, 11, 12, …, 36 ]
  63. 63. 61 start = [ 0, 5, 6, 7, 9, 11, …, 35 ] end = [ 5, 6, 7, 9, 11, 12, …, 36 ] …
  64. 64. 62 start = [ 0, 5, 6, 7, 9, 11, …, 35 ] end = [ 5, 6, 7, 9, 11, 12, …, 36 ] offset = [ 0, 5, 6, 7, 9, 11, …, 35, 36 ] start = offset[i] end = offset[i + 1] + =
  65. 65. 63 [ { type: 46, start: 0, end: 1, line: 1, column: 1 }, … ] Uint8Array Uint32Array Uint32Array Uint32Array Uint32Array 1 4 4 4 4 13 per token 983 085 x 13 = 12.7Mb
  66. 66. 64 a { top: 0; } lines = [ 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3 ] columns = [ 1, 2, 3, 4, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1 ] lines & columns
  67. 67. 64 a { top: 0; } lines = [ 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3 ] columns = [ 1, 2, 3, 4, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1 ] lines & columns
  68. 68. 65 line = lines[offset]; column = offset - lines.lastIndexOf(line - 1, offset); lines & columns
  69. 69. 65 line = lines[offset]; column = offset - lines.lastIndexOf(line - 1, offset); lines & columns Ок для коротких строк, нужно кешировать для длинных
  70. 70. 66 [ { type: 46, start: 0, end: 1, line: 1, column: 1 }, … ] Uint8Array Uint32Array Uint32Array Uint32Array Uint32Array 1 4 4 4 4 9 per token 983 085 x 9 = 8.8Mb
  71. 71. 67 8.8Mb vs. 12.7Mb(min)
  72. 72. Меньше операций со строками
  73. 73. «Убийцы» производительности* • RegExp • Конкатенация строк • toLowerCase/toUpperCase • substr/substring • … 69 * Засоряют GC и он все портит
  74. 74. «Убийцы» производительности* • RegExp • Конкатенация строк • toLowerCase/toUpperCase • substr/substring • … 70 Без этого никак, 
 но от остального можно избавиться * Засоряют GC и он все портит
  75. 75. 71 var start = scanner.tokenStart; … scanner.next(); … scanner.next(); … return source.substr(start, scanner.tokenEnd); Нет конкатенации!
  76. 76. 72 function cmpStr(source, start, end, str) { if (end - start !== str.length) { return false; } for (var i = start; i < end; i++) { var sourceCode = source.charCodeAt(i); var strCode = str.charCodeAt(i - start); if (sourceCode !== strCode) { return false; } } return true; } Сравнение строк
  77. 77. 73 function cmpStr(source, start, end, str) { if (end - start !== str.length) { return false; } for (var i = start; i < end; i++) { var sourceCode = source.charCodeAt(i); var strCode = str.charCodeAt(i - start); if (sourceCode !== strCode) { return false; } } return true; } Сравнение строк Быстрое отсечение по длине
  78. 78. 74 function cmpStr(source, start, end, str) { if (end - start !== str.length) { return false; } for (var i = start; i < end; i++) { var sourceCode = source.charCodeAt(i); var strCode = str.charCodeAt(i - start); if (sourceCode !== strCode) { return false; } } return true; } Сравнение строк Сравниваем 
 код за кодом
  79. 79. Как сравнивать 
 без учета регистра*? 75 * То есть без toLowerCase/toUpperCase
  80. 80. Эвристика • Сравниваем с заранее известными строками (str) • Заранее заданные строки всегда в нижнем регистре и содержат только латинские буквы • Читал я как то в твиттере… 76
  81. 81. Чтобы перевести из верхнего регистра в нижний, нужно выставить 6-й бит в 1 (работает только для латинских букв) 'A' = 01000001 'a' = 01100001 'A'.charCodeAt(0) | 32 === 'a'.charCodeAt(0) 77
  82. 82. 78 function cmpStr(source, start, end, str) { … for (var i = start; i < end; i++) { … // source[i].toLowerCase() if (sourceCode >= 65 && sourceCode <= 90) { // 'A' .. 'Z' sourceCode = sourceCode | 32; } if (sourceCode !== strCode) { return false; } } … } Сравнение строк без учета регистра
  83. 83. Бенефиты • Часто срабатывает быстрое отсечение • Нет получения подстрок (не давим на CG) • Нет получения временных строк 
 (результат toLowerCase/toUpperCase) • Операция сравнения не производит мусор 79
  84. 84. Отказываемся от массивов (от слова совсем)
  85. 85. Что не так с массивами • Если растить массив, то происходит копирование памяти + нагрузка на GC • Мы не можем заранее знать размер массива 81
  86. 86. Решение? 82
  87. 87. Двусвязные списки 83
  88. 88. 84
  89. 89. Плюсы • Не вызывает копирование памяти • Не засоряет CG при построении AST • Мы получаем next/prev 85
  90. 90. Всё это и многое другое позволило уменьшить потребление памяти, нагрузку на GC 
 и ускорить вдвое 86
  91. 91. Но это еще не конец 😋 87
  92. 92. История ускорения #3 неделя после FrontTalks
  93. 93. Общие моменты • Упрощение структуры AST • Меньше потребление памяти, переиспользование • list.map().join() -> цикл + конкатенация • и по мелочи… 89
  94. 94. И снова про стоимость токенов
  95. 95. 91 [ { type: 46, start: 0, end: 1, line: 1, column: 1 }, … ] Uint8Array Uint32Array Uint32Array Uint32Array Uint32Array 1 types 4 offsets 4 4 lines 4 9 per token 983 085 x 9 = 8.8Mb
  96. 96. lines можно считать не всегда и лениво 92
  97. 97. 93 [ { type: 46, start: 0, end: 1, line: 1, column: 1 }, … ] Uint8Array Uint32Array Uint32Array Uint32Array Uint32Array 1 types 4 offsets 4 4 lines 4 5 per token 983 085 x 5 = 4.9Mb
  98. 98. Действительно ли для offsets нужно 32 бита? Эвристика: вряд ли кто-то будет парсить CSS больше 16Mb 94
  99. 99. 95 offset = [ 0, 5, 6, 7, 9, 11, 11, …, 1234 ] type = [ 1, 47, 47, 4, 4, 47, 5, …, 3 ]
  100. 100. 96 offset = [ 0, 5, 6, 7, 9, 11, 11, …, 1234 ] type = [ 1, 47, 47, 4, 4, 47, 5, …, 3 ] offsetAndType[i] = type[i] << 24 | offset[i] + =
  101. 101. 97 offset = [ 0, 5, 6, 7, 9, 11, 11, …, 1234 ] type = [ 1, 47, 47, 4, 4, 47, 5, …, 3 ] offsetAndType[i] = type[i] << 24 | offset[i] offsetAndType = [ 16777216, 788529157, … ] + =
  102. 102. 98 offset = [ 0, 5, 6, 7, 9, 11, 11, …, 1234 ] type = [ 1, 47, 47, 4, 4, 47, 5, …, 3 ] offsetAndType[i] = type[i] << 24 | offset[i] offsetAndType = [ 16777216, 788529157, … ] offset = offsetAndType[i] & 0xFFFFFF; type = offsetAndType[i] >> 24; + =
  103. 103. 99 [ { type: 46, start: 0, end: 1, line: 1, column: 1 }, … ] Uint8Array Uint32Array Uint32Array Uint32Array Uint32Array 1 types 4 offsets 4 4 lines 4 4 per token 983 085 x 4 = 3.9Mb
  104. 104. 3.9-7.8 Mb vs. 12.7 Mb (min) 100
  105. 105. 101 class Scanner { ... next() { var next = this.currentToken + 1; this.currentToken = next; this.tokenStart = this.tokenEnd; this.tokenEnd = this.offsetAndType[next + 1] & 0xFFFFFF; this.tokenType = this.offsetAndType[next] >> 24; } } Два чтения из массива – 
 как то не круто…
  106. 106. 102 offset = [ 0, 5, 6, 7, 9, 11, 11, …, 1234 ] type = [ 1, 47, 47, 4, 4, 47, 5, …, 3 ]
  107. 107. 103 offset = [ 0, 5, 6, 7, 9, 11, 11, …, 1234 ] type = [ 0, 1, 47, 47, 4, 4, 47, 5, …, 3 ]
  108. 108. 103 offset = [ 0, 5, 6, 7, 9, 11, 11, …, 1234 ] type = [ 0, 1, 47, 47, 4, 4, 47, 5, …, 3 ] …
  109. 109. 104 class Scanner { ... next() { var next = this.currentToken + 1; this.currentToken = next; this.tokenStart = this.tokenEnd; this.tokenEnd = this.offsetAndType[next + 1] & 0xFFFFFF; this.tokenType = this.offsetAndType[next + 1] >> 24; } } Теперь можно в одно чтение
  110. 110. 105 class Scanner { ... next() { var next = this.currentToken + 1; this.currentToken = next; this.tokenStart = this.tokenEnd; next = this.offsetAndType[next + 1]; this.tokenEnd = next & 0xFFFFFF; this.tokenType = next >> 24; } } -50% чтений (~250k)
  111. 111. Переиспользование
  112. 112. Сканер каждый раз создавал новые массивы на каждый разбор 107
  113. 113. Сканер каждый раз создавал новые массивы на каждый разбор 107
  114. 114. Новая стратегия • По дефолту создается буфер в 16Kb • Создается новый буфер, только если он мал для разбираемого CSS • Значительный прирост скорости, особенно в сценариях разбора малых фрагментов CSS 108
  115. 115. 109 CSSTree: 24 ms Mensch: 31 ms CSSOM: 36 ms PostCSS: 38 ms Rework: 81 ms PostCSS Full: 100 ms Gonzales: 175 ms Stylecow: 176 ms Gonzales PE: 214 ms ParserLib: 414 ms bootstrap.css v3.3.7 (146Kb) github.com/postcss/benchmark 13 ms 7 ms Текущий результат
  116. 116. И это еще не конец… 😋 110
  117. 117. Минутка «рекламы»
  118. 118. CSSTree – 
 не только про скорость 112
  119. 119. Новая фича*: Разбор и матчинг синтаксиса CSS значений 113 * Пока уникальная среди CSS парсеров
  120. 120. Пример 114
  121. 121. 115 csstree.github.io/docs/syntax.html Документация синтаксиса
  122. 122. 116 csstree.github.io/docs/validator.html Валидатор синтаксиса CSS значений
  123. 123. 117 var csstree = require('css-tree'); var syntax = csstree.syntax.defaultSyntax; var ast = csstree.parse('… your css …'); csstree.walkDeclarations(ast, function(node) { if (!syntax.match(node.property.name, node.value)) { console.log(syntax.lastMatchError); } }); Свой валидатор в 8 строк
  124. 124. Кое что еще • csstree-validator – npm пакет + консольная команда • stylelint-csstree-validator – плагин для stylelint • gulp-csstree – плагин для gulp • SublimeLinter-contrib-csstree – плагин для Sublime Text • vscode-csstree – плагин для VS Code • csstree-validator – плагин для Atom
 
 More is coming… 118
  125. 125. Заключение
  126. 126. Хотите чтобы ваш JavaScript работал так же быстро как Си, сделайте его похожим на Си 120
  127. 127. Изучайте алгоритмы, структуры данных, как работают JS-движки и GC – 
 у вас будет больше вариантов для оптимизаций 121 – К.О.
  128. 128. Доклады по теме • CSSO – история ускорения
 tinyurl.com/csso-speedup • Парсим CSS
 tinyurl.com/csstree-intro 122
  129. 129. github.com/csstree/csstree 123 Нужен ваш фидбек
  130. 130. Роман Дворнов @rdvornov github.com/lahmatiy rdvornov@gmail.com Вопросы? github.com/csstree/csstree

«Парсим CSS», Роман Дворнов (Avito) В ходе работы над CSSO мне пришлось погрузиться в процесс парсинга CSS. В результате парсер (тот, что в CSSO) был не раз переписан. Пришло время сделать его отдельным инструментом. Новый быстрый детальный парсер CSS, его AST, области применения и кое-что ещё.

Views

Total views

7,211

On Slideshare

0

From embeds

0

Number of embeds

6,793

Actions

Downloads

8

Shares

0

Comments

0

Likes

0

×