В этих двух статьях я буду сравнивать TailwindCSS с чистым CSS + BEM. Цель - разобраться что является лучшим решением для хорошей архитектуры приложения. Это не вопрос предпочтений, от этого выбора будет зависеть очень многое на поздних этапах разработки и оно должно быть очень хорошо обосновано. Начну со сравнения производительности. Tailwind позволяет значительно уменьшить размер итогового CSS и тем самым ускорить время отображения страницы. Но это сработает только в том случае, если Tailwind классы будут написаны прямо в HTML коде, а не в виде @apply в CSS. Tailwind уменьшает CSS, но увеличивает HTML. Давайте посчитаем разницу с учетом HTML. Будем сравнивать чистый Tailwind с чистым CSS + BEM.
Расчеты
На каждый Tailwind класс - одно свойство. На каждый HTML элемент - полтора BEM класса (если учитывать модификаторы).
lt - средняя длина Tailwind класса lc - средняя длина BEM класса lp - средняя длина CSS свойства Pu - количество уникальных свойств P - среднее количество свойств на элемент C - среднее количество BEM классов на элемент E - количество элементов Eu - количество уникальных элементов Структура CSS для Tailwind: .lt * Pu { lp * Pu; } Структура CSS для BEM: .lc * Eu * C { lp * Eu * P; } Структура HTML для Tailwind: <div class="lt * E * P"> Структура HTML для BEM <div class="lc * E * C"> Считаем общий размер: Tailwind: lt * Pu + lp * Pu + lt * E * P BEM: lc * Eu * C + lp * Eu * P + lc * E * C
Функция для экспериментов в браузере:
function calc({lt, lc, lp, Pu, P, C, E, Eu}) { const Tailwind = lt * Pu + lp * Pu + lt * E * P const BEM = lc * Eu * C + lp * Eu * P + lc * E * C return { Tailwind, BEM, diff: Tailwind / BEM } }
Для анализа я взял главную страницу GitHub после логина. Вот некоторые инструменты для анализа:
// Количество элементов на сайте: console.log(Array.from(document.querySelectorAll('[class]')).length) // Количество уникальных (по классам) элементов на сайте: console.log(Array.from(document.querySelectorAll('[class]')) .reduce((a, o) => { a.add(Array.from(o.classList.values()).sort().join(' ')) return a }, new Set()).size)
Страница https://github.com/ 3164 элементов 502 уникальных элементов 12101 всех CSS свойств 3956 уникальных CSS свойств 404088 общая длина всех CSS свойств 163733 общая длина уникальных CSS свойств 33 средняя длина всех CSS свойств 41 средняя длина уникальных CSS свойств 24 свойства на элемент
calc({ lt: 10, // средняя длина Tailwind класса lc: 35, // средняя длина BEM класса lp: 35, // средняя длина CSS свойства Pu: 3956, // количество уникальных свойств P: 24, // среднее количество свойств на элемент C: 1.5, // среднее количество BEM классов на элемент E: 3164, // количество элементов Eu: 502, // количество уникальных элементов }) // Tailwind: 937380, // BEM: 614145, // Tailwind / BEM: 1.5263170749578682
Размер Tailwind в 1.5 раза больше
Оптимизация
Для BEM мы могли бы сделать оптимизацию CSS и для каждого свойства прописать все классы с этим свойства в селекторе. Тогда общая длина всех свойств должна уменьшиться:
.lc * Eu * C * P { lp * Eu; }
Формула для BEM будет такой:
lc * Eu * C * P + lp * Eu + lc * E * C
Рассмотрим тот же GitHub, но с обфускацией имен классов и оптимизацией:
function calc({lt, lc, lp, Pu, P, C, E, Eu}) { const Tailwind = lt * Pu + lp * Pu + lt * E * P const BEM = lc * Eu * C * P + lp * Eu + lc * E * C return { Tailwind, BEM, diff: Tailwind / BEM } } calc({ lt: 5, // средняя длина Tailwind класса (с обфускацией) lc: 5, // средняя длина BEM класса (с обфускацией) lp: 35, // средняя длина CSS свойства Pu: 3956, // количество уникальных свойств P: 24, // среднее количество свойств на элемент C: 1.5, // среднее количество BEM классов на элемент E: 3164, // количество элементов Eu: 502, // количество уникальных элементов }) // Tailwind: 537920 // BEM: 131660 // Tailwind / BEM: 4.085675224061978
С оптимизацией и обфускацией размер Tailwind уже в 4 раз больше BEM.
Но, к сожалению, такая оптимизация невозможна. Этот пример иллюстрирует почему:
<style> .a { color: red; } .b { color: blue; } .c { color: red; } </style> <div class="a b"></div> <!-- Blue --> <div class="b c"></div> <!-- Red --> ========================= <style> .a, .c { color: red; } .b { color: blue; } </style> <div class="a b"></div> <!-- Blue --> <div class="b c"></div> <!-- Blue --> ========================= <style> .b { color: blue; } .a, .c { color: red; } </style> <div class="a b"></div> <!-- Red --> <div class="b c"></div> <!-- Red -->
Но обфускация имен классов возможна. Рассмотрим случай без невозможной оптимизации, но с обфускацией.
calc({ lt: 5, // средняя длина Tailwind класса (с обфускацией) lc: 5, // средняя длина BEM класса (с обфускацией) lp: 35, // средняя длина CSS свойства Pu: 3956, // количество уникальных свойств P: 24, // среднее количество свойств на элемент C: 1.5, // среднее количество BEM классов на элемент E: 3164, // количество элементов Eu: 502, // количество уникальных элементов }) // Tailwind: 537920, // BEM: 449175, // Tailwind / BEM: 1.1975733288807258
Еще, размер ��ласса Tailwind намного сильнее влияет на общий размер HTML/CSS, чем размер класса BEM. И это понятно, ведь общее число элементов на странице в 7 раз больше уникальных.
Важен ли размер CSS/HTML и на сколько?
Насколько вообще большим может быть CSS? На BEM не сложно получить 3 мегабайта. Но gzip сжимает 3MB до всего 60kB. Конечно, после скачивания браузеру нужно еще распаковать CSS, а затем проанализировать, что занимает какое-то время. По моим тестам это 0.5-1 сек на десктопе. На мобильных устройствах это время должно быть значительно больше. Быстрая загрузка и быстрый анализ CSS/HTML важнее, чем изображения или даже скрипты. Скриптам дается небольшая фора, т.к. пользователь не сразу взаимодействует со страницей. А без HTML/CSS страница даже не отобразится корректно, и не начнется загрузка изображений.
Скрипт для анализа конкретного сайта
Запусти этот скрипт в консоли для любого сайта и ты узнаешь разницу в размерах между Tailwind и BEM.
Запускать желательно отключив защиту браузера, чтобы скрипт учел все стили загружаемые из CDN:
chrome.exe --disable-web-security --user-data-dir="C:/Temp/ChromeDevSession"
Алгоритм не учитывает каскадные стили, т.е все которые содержат символы " >+~". Учесть их очень сложно, поэтому лучше просто выбирать качественно сверстанные сайты, где каскада немного. Так же алгоритм считает, что для BEM для каждого уникального элемента есть своя уникальная таблица стилей. Поэтому в реальности размер CSS для BEM будет меньше чем оцениваемый алгоритмом.
В расчетах не учитывается текущий способ именования классов. На результат влияет только качество дизайна и его верстки. Utility first подход заставляет делать дизайн и верстку более качественной, т.е. придерживаться определенных заранее наборов стилей. Поэтому наверное сайты сверстанные на Tailwind показывают лучшие результаты по размеру HTML/CSS.
Скрипт выполняется примерно минуту из-за функции element.matches(rule.selectorText). Я не знаю как это оптимизировать.
function walkRules(rules, callback, mediaRule) { Array.from(rules).forEach(rule => { if (rule instanceof CSSStyleRule) { callback(rule, mediaRule) } else if (rule instanceof CSSMediaRule) { walkRules(rule.cssRules, callback, rule) } }) } function getRules() { const rules = [] Array.from(document.styleSheets).forEach(sheet => { // ignore cross-origin stylesheets try { sheet.cssRules } catch (err) { console.warn(`Stylesheet ${sheet.href} is not accessible`) return } walkRules(sheet.cssRules, (rule, mediaRule) => { if ( !/\.[\w-]/.test(rule.selectorText) || // only class selectors /[ >+~]/.test(rule.selectorText) // exclude cascading selectors ) { return } const style = {} for (let i = 0; i < rule.style.length; i++) { const key = rule.style[i] style[key] = rule.style.getPropertyValue(key).trim().toLowerCase() } rules.push({ rule, mediaRule, style }) }) }) return rules } function getElements(rules) { const allElements = Array.from(document.querySelectorAll('[class]')) const filteredElements = [] const time0 = Date.now() console.log('getElements START') let logTime = Date.now() const nonVisualTags = ['SCRIPT', 'STYLE', 'LINK', 'META', 'TITLE', 'NOSCRIPT'] allElements.forEach((element, i) => { if (nonVisualTags.includes(element.tagName)) { return } const elementRules = rules.filter(({ rule }) => { return element.matches(rule.selectorText) }) if (elementRules.length === 0) { return } filteredElements.push({ element, rules: elementRules }) if (Date.now() - logTime > 1000) { console.log( `getElements: ${((i / allElements.length) * 100).toFixed(2)}% ${ (Date.now() - time0) / 1000 }s`, ) logTime = Date.now() } }) console.log('getElements END', performance.now() - time0) return filteredElements } function calcStat() { const rules = getRules() const elements = getElements(rules) const uniqueElements = Array.from( elements .reduce((a, e) => { const key = e.rules.map(({ rule }) => rule.selectorText).join(' ') if (!a.has(key)) { a.set(key, e) } return a }, new Map()) .values(), ) const stat = { total: { html: { elementsCount: 0, }, css: { selectorCount: 0, mediaCount: 0, mediaSize: 0, styleSize: 0, propCount: 0, }, }, tailwind: { html: { classCount: 0, }, css: { selectorCount: 0, styleSize: 0, propCount: 0, }, }, bem: { html: { elementsCount: 0, }, css: { selectorCount: 0, mediaCount: 0, mediaSize: 0, styleSize: 0, propCount: 0, }, }, } const uniqueProps = uniqueElements.reduce((a, e) => { e.rules.forEach(({ style }) => { Object.keys(style).forEach(key => { a.add(key + ':' + style[key] + ';') }) }) return a }, new Set()) // Total stat.total.html.elementsCount = elements.length rules.forEach(({ rule, mediaRule, style }) => { Object.keys(style).forEach(key => { stat.total.css.propCount++ const prop = key + ':' + style[key] + ';' stat.total.css.styleSize += prop.length }) stat.total.css.selectorCount++ if (mediaRule) { stat.total.css.mediaCount++ stat.total.css.mediaSize += mediaRule.media.mediaText.trim().length } }) // Tailwind stat.tailwind.css.selectorCount = uniqueProps.size stat.tailwind.css.propCount = uniqueProps.size uniqueProps.forEach(prop => { stat.tailwind.css.styleSize += prop.length }) elements.forEach(({ element, rules }) => { const elemUniqueProps = new Set() rules.forEach(({ rule, mediaRule, style }) => { Object.keys(style).forEach(key => { elemUniqueProps.add( key + ':' + style[key] + ';' + (mediaRule?.media?.mediaText?.trim() || ''), ) }) }) if (!elemUniqueProps.size) { debugger } stat.tailwind.html.classCount += elemUniqueProps.size }) // BEM stat.bem.html.elementsCount = elements.length stat.bem.css.selectorCount = uniqueElements.length uniqueElements.forEach(({ element, rules }) => { const uniqueMedia = new Set() rules.forEach(({ rule, mediaRule, style }) => { const mediaText = mediaRule?.media?.mediaText?.trim() || '' if (mediaText) { uniqueMedia.add(mediaText) } Object.keys(style).forEach(key => { const prop = key + ':' + style[key] + ';' stat.bem.css.styleSize += prop.length stat.bem.css.propCount++ }) }) stat.bem.css.mediaCount += uniqueMedia.size uniqueMedia.forEach(media => { stat.bem.css.mediaSize += media.length }) }) return stat } var stat = calcStat() console.log(JSON.stringify(stat, null, 2)) function tailwindVsBem({ stat, bemClassSize, bemClassesPerElement, tailwindClassSize, }) { const bemCssSize = stat.bem.css.styleSize + stat.bem.css.mediaSize + stat.bem.css.selectorCount * bemClassSize + stat.bem.css.mediaCount * bemClassSize const bemHtmlSize = stat.bem.html.elementsCount * bemClassSize * bemClassesPerElement const tailwindCssSize = stat.tailwind.css.styleSize + stat.tailwind.css.selectorCount * tailwindClassSize const tailwindHtmlSize = stat.tailwind.html.classCount * tailwindClassSize const bemSize = bemCssSize + bemHtmlSize const tailwindSize = tailwindCssSize + tailwindHtmlSize const diff = tailwindSize / bemSize console.log(` ${document.location.href} Tailwind = ${tailwindCssSize} (CSS) + ${tailwindHtmlSize} (HTML) = ${tailwindSize} BEM = ${bemCssSize} (CSS) + ${bemHtmlSize} (HTML) = ${bemSize} Tailwind / BEM = ${diff} `) } tailwindVsBem({ stat, bemClassSize: 35, bemClassesPerElement: 1.5, tailwindClassSize: 10, })
Результаты
Страницы на чистом CSS:
https://www.youtube.com Tailwind = 38936 (CSS) + 347670 (HTML) = 386606 BEM = 129424 (CSS) + 202230 (HTML) = 331654 Tailwind / BEM = 1.1656907499984923 https://dzen.ru/ Tailwind = 19449 (CSS) + 165240 (HTML) = 184689 BEM = 80186 (CSS) + 75232.5 (HTML) = 155418.5 Tailwind / BEM = 1.1883334352088073 https://habr.com/ru/articles/774524/ Tailwind = 1173 (CSS) + 370 (HTML) = 1543 BEM = 946 (CSS) + 157.5 (HTML) = 1103.5 Tailwind / BEM = 1.3982782057091074
Страницы на Tailwind или другом Utility First подходе:
https://github.com (Микс чистого CSS и Tailwind) Tailwind = 25103 (CSS) + 482850 (HTML) = 507953 BEM = 155295 (CSS) + 155190 (HTML) = 310485 Tailwind / BEM = 1.6359985184469459 https://stackoverflow.com/questions/588004/is-floating-point-math-broken/588014 Tailwind = 25317 (CSS) + 449170 (HTML) = 474487 BEM = 132373 (CSS) + 140437.5 (HTML) = 272810.5 Tailwind / BEM = 1.7392549040451155 https://www.facebook.com/random_user Tailwind = 42286 (CSS) + 400300 (HTML) = 442586 BEM = 378248 (CSS) + 170415 (HTML) = 548663 Tailwind / BEM = 0.8066627419745819 https://tailwindcss.com Tailwind = 22325 (CSS) + 159330 (HTML) = 181655 BEM = 120739 (CSS) + 121800 (HTML) = 242539 Tailwind / BEM = 0.7489723302231805 https://www.shopify.com Tailwind = 25053 (CSS) + 102080 (HTML) = 127133 BEM = 179634 (CSS) + 78750 (HTML) = 258384 Tailwind / BEM = 0.4920312403244783 https://www.netflix.com/tudum/top10/ Tailwind = 13288 (CSS) + 80360 (HTML) = 93648 BEM = 48737 (CSS) + 60375 (HTML) = 109112 Tailwind / BEM = 0.8582740670137107 https://io.google/2022/ Tailwind = 11124 (CSS) + 27390 (HTML) = 38514 BEM = 43079 (CSS) + 21682.5 (HTML) = 64761.5 Tailwind / BEM = 0.5947051874956572 https://dotnet.microsoft.com/en-us/ Tailwind = 15199 (CSS) + 27520 (HTML) = 42719 BEM = 49361 (CSS) + 13860 (HTML) = 63221 Tailwind / BEM = 0.6757090207367805
Выводы
Tailwind уменьшает размер HTML/CSS только если вся веб страница (и дизайн, и верстка) проектируется с использованием Utility First подхода. Об этом конечно должны договориться все: и дизайнер, и верстальщик, и заказчик. При очень хорошем проектировании и верстке можно получить размер в 2 раза меньше чем при верстке на BEM.
Если есть произвольный макет и нужно верстать в Pixel Perfect, то Tailwind скорее увеличит размер в 1.5 раза. Так же Tailwind не поможет если на странице очень много повторяющихся элементов (например сайт StackOverflow)
P.S.
Tailwind врет о феноменальном увеличении производительности. (В этой статье он учитывает только размер файлов для загрузки). Он пишет, что на Netflix всего 10KB CSS, но ни слова не говорит об увеличившемся размере HTML. Общая длина всех атрибутов "class" равна 87231. Это чуть больше, чем получилось по моим расчетам здесь. На самом деле Tailwind сэкономил только 15% общего размера несжатого HTML+CSS.
Вот скрипт для вычисления общего размера классов в HTML:
Array.from(document.querySelectorAll('[class]')) .reduce((a, o) => { a += o.getAttribute('class').length return a }, 0)
Буду рад услышать любую критику, замечания, ваш личный опыт, ...
