Icon. Оверинжиниринг или необходимость?
Кто я такой?
ex-HeadHunter
Работал в команде архитектуры
— Не только перекладываю файлы по папкам
— Помогаю разработчикам писать код
Кто я такой?
ex-HeadHunter
Работал в команде архитектуры
— Не только перекладываю файлы по папкам
— Помогаю разработчикам писать код
Да я же FrontУпс!
Кто я такой?
ex-HeadHunter
Работал в команде архитектуры
— Не только перекладываю файлы по папкам
— Помогаю разработчикам писать код
Да я же FrontУпс!
Кстати, HeadHunter ищет разработчиков в продуктовую команду
*
Дизайнеры
Базовые иконки
Базовые иконки
Базовые иконки
Базовые иконки
Базовые иконки
(не)Базовые иконки
(не)Базовые иконки
(не)Базовые иконки
Надо понимать 2 вещи
Надо понимать 2 вещи
Дизайнеры — хорошие
Это все лирика
1. Есть базовые иконки в UIKit
1. Есть базовые иконки в UIKit
2. Есть кастомные иконки
1. Есть базовые иконки в UIKit
2. Есть кастомные иконки
3. Размер-цвет отличаются
1. Есть базовые иконки в UIKit
2. Есть кастомные иконки
3. Размер-цвет отличаются
4. Большинство иконкок одноцветные
Что будем делать?
1. Разберем разные способы
1. Разберем разные способы
2. Возьмем один модный и популярный
1. Разберем разные способы
2. Возьмем один модный и популярный
3. Проанализируем
1. Разберем разные способы
2. Возьмем один модный и популярный
3. Проанализируем
4. Постараемся избавиться от минусов
У всех способов есть плюсы и минусы, особенный только один
Скрин-ридеры
Скрин-ридеры
Скрин-ридеры
Их отображение зависит от:
Системы, браузера, настроек системы, настроек видеокарты и положения луны относительно солнца
Можно ненадо
Что еще?
1. Спрайты
<svg width="124" height="16" viewBox="0 0 124 16"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<symbol id="filters" width="16" height="16">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1 4.667c0-.392.317-.709.708-.709h6a.708.708 0 1 1 0 1.417h-6A.708.708 0 0 1 1 4.667zM12.334 4.667c0-.392.317-.709.708-.709h1.333a.708.708 0 0 1 0 1.417h-1.333a.708.708 0 0 1-.708-.708z"/>
</symbol>
<use x="0" y="0" xlink:href="#filters" fill="#3d3e42"/>
<use x="18" y="0" xlink:href="#filters" fill="#fff"/>
</svg>
Спрайты
Спрайты
👎 Анимации https://codepen.io/collection/DYwzZe
Спрайты
👎 Анимации https://codepen.io/collection/DYwzZe
👎 Количество вариаций ограниченно текущими CSS классами. Приходится либо расширять все, либо костылять нужные иконки
Спрайты
👎 Анимации https://codepen.io/collection/DYwzZe
👎 Количество вариаций ограниченно текущими CSS классами. Приходится либо расширять все, либо костылять нужные иконки
👍 Кэш
Спрайты
👎 Анимации https://codepen.io/collection/DYwzZe
👎 Количество вариаций ограниченно текущими CSS классами. Приходится либо расширять все, либо костылять нужные иконки
👍 Кэш
👍 Цвета
Что еще?
1. Спрайты
2. CSS-Filters
CSS-Filters
.A {
filter: invert(30%) sepia(93%) saturate(7246%) hue-rotate(356deg)
brightness(102%) contrast(120%);
}
.B {
filter: invert(10%) sepia(99%) saturate(4712%) hue-rotate(243deg)
brightness(108%) contrast(145%);
}
.C {
filter: invert(54%) sepia(48%) saturate(1714%) hue-rotate(79deg)
brightness(113%) contrast(126%);
}
CSS-Filters
.red {
filter: invert(30%) sepia(93%) saturate(7246%) hue-rotate(356deg)
brightness(102%) contrast(120%);
}
.blue {
filter: invert(10%) sepia(99%) saturate(4712%) hue-rotate(243deg)
brightness(108%) contrast(145%);
}
.green {
filter: invert(54%) sepia(48%) saturate(1714%) hue-rotate(79deg)
brightness(113%) contrast(126%);
}
CSS-Filters
.red {
filter: invert(30%) sepia(93%) saturate(7246%) hue-rotate(356deg)
brightness(102%) contrast(120%);
}
.blue {
filter: invert(10%) sepia(99%) saturate(4712%) hue-rotate(243deg)
brightness(108%) contrast(145%);
}
.green {
filter: invert(54%) sepia(48%) saturate(1714%) hue-rotate(79deg)
brightness(113%) contrast(126%);
}
Согласны на ревью такого?
CSS-Filters
👍 Анимация
CSS-Filters
👍 Анимация
👍 Кэш
CSS-Filters
👍 Анимация
👍 Кэш
👎 Одноцветные иконки
CSS-Filters
👍 Анимация
👍 Кэш
👎 Одноцветные иконки
👎 Не ревью-френдли
CSS-Filters
Что еще?
1. Спрайты
2. CSS-Filters
3. CSS-Mask
CSS-Mask
.clipped {
clip-path: polygon(50% 0%, 35% 63%, 82% 100%, 18% 100%, 0% 38%);
}
CSS-Mask
.clipped {
clip-path: polygon(50% 0%, 35% 63%, 82% 100%, 18% 100%, 0% 38%);
}
CSS-Mask
.clipped {
clip-path: polygon(50% 0%, 35% 63%, 82% 100%, 18% 100%, 0% 38%);
}
.masked {
mask: url(mask.svg);
}
CSS-Mask
Что еще?
1. Спрайты
2. CSS-Filters
3. CSS-Mask
4. inline
5. inline + use
Inline
Inline
<svg width="50" height="40" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg">
<g>
<path fill="none" d="M0,0h48v48h-48Z"></path>
<path fill="#ff0000" d="M24,9c-10,0 -18.54,6.22 -22,15c3.46,8.78 12,15 22,15c10,0 18.54,-6.22 22,-15c-3.46,-8.78 -11.99,-15 -22,-15Zm0,25c-5.52,0 -10,-4.48 -10,-10c0,-5.52 4.48,-10 10,-10c5.52,0 10,4.48 10,10c0,5.52 -4.48,10 -10,10Zm0,-16c-3.31,0 -6,2.69 -6,6c0,3.31 2.69,6 6,6c3.31,0 6,-2.69 6,-6c0,-3.31 -2.69,-6 -6,-6Z"></path>
</g>
</svg>
Inline
Inline
👎 SSR не френдли?
Inline
👎 SSR не френдли?
👎 Как разделять код компонента и иконки?
Inline
Base64 более тяжелый, чем просто иконка, поэтому для простоты будем считать что одна иконка ≈ 400 байт
Inline
Inline
Inline
Inline
Inline
Inline
Inline
Inline
Inline
Inline
Inline
10
Inline
10 + 2?
Inline
👎 SSR не френдли? — кэш + дублирование
Inline
Inline
10 + 7 + 2? + 1?
Inline
(10 + 7 + 2+ 1) * 400 ≈ 8 Кб
Этот код будет загружен до FMP
Inline
👎 SSR не френдли?
кэш
дублирование
размер при большом количестве иконок на странице
8 Килобайт, зачем страдать?
(100 + чуть-чуть) * почти полкилобайта = Около 40, а то и 50 Кб
Код, вставленный as is в js компонент, не соптимизируется svgo
Нужно прогонять иконки в svgo, прежде чем вставить на страницу!
А я в себя не верю
Inline
👎 SSR не френдли?
кэш
дублирование
размер при большом количестве иконок на странице
Inline
👎 SSR не френдли?
кэш
дублирование
размер при большом количестве иконок на странице
👎 Как разделять код компонента и иконки?
Оптимизации
👎 SSR не френдли?
кэш
дублирование
размер при большом количестве иконок на странице
👎 Как разделять код компонента и иконки?
Логика
Давайте начнем с логики
Теперь будет (хард)код
Разделение
function Icon({ fill, width = 50, height = 40, additionalClassName }) {
return (
<svg width={width} height={height}>
<g className={classnames('icon', additionalClassName)}>
<path fill="none" d="M0,0h48v48h-48Z"></path>
<path fill={fill} d="M24,9c-10,0 -18.54,6.22 -22,15c3.46,8.78 12,15 22,15c10,0 18.54,-6.22 22,-15c-3.46,-8.78 -11.99,-15 -22,-15Zm0,25c-5.52,0 -10,-4.48 -10,-10c0,-5.52 4.48,-10 10,-10c5.52,0 10,4.48 10,10c0,5.52 -4.48,10 -10,10Zm0,-16c-3.31,0 -6,2.69 -6,6c0,3.31 2.69,6 6,6c3.31,0 6,-2.69 6,-6c0,-3.31 -2.69,-6 -6,-6Z"></path>
</g>
</svg>
);
}
Всё ещё бойлерплейтишь? автоматизируй!
Идея
const fs = require('fs');
const { getOptions } = require('loader-utils');
module.exports = function(source) {
if (this.cacheable) {
this.cacheable();
}
const options = getOptions(this);
const template = fs.readFileSync(options.template, 'utf8');
return template.replace(
'<icon />',
source
.replace('{width}', 'width={width}')
.replace('{height}', 'height={height}')
.replace('class="{class}"', 'className={classNames}')
.replace('{fill}', 'fill={fill}')
.replace('{stroke}', 'stroke={stroke}');
);
};
function Icon({
id, fill, stroke, loading,
width = 16, height = 16, highlight, disabledColor,
additionalClasses = []
}) {
const classNames = classnames('bloko-icon', additionalClasses, {
'bloko-icon_loading': loading,
[`bloko-icon_highlight-${highlight}`]: highlight,
[`bloko-icon_disabled`]: disabledColor,
});
return (
<icon />
);
}
Преимущества
👍 Можно автоматизированно запускать SVGO-loader
Преимущества
👍 Можно автоматизированно запускать SVGO-loader
👍 Логика разделена, не нужно бойлерплейтить
Преимущества
👍 Можно автоматизированно запускать SVGO-loader
👍 Логика разделена, не нужно бойлерплейтить
👍 Кастомизируемо:
Разные цвета
Анимации
Все что угодно
Это решение не помогает SSR
* В плане кэша, дублирования и размеров
SSR-френдли
<svg width={width} height={height}>
<g className={classnames('icon', additionalClassName)}>
<path fill="none" d="M0,0h48v48h-48Z"></path>
<path fill={fill} d="M24,9c-10,0 -18.54,6.22 -22,15c3.46,8.78 12,15 22,15c10,0 18.54,-6.22 22,-15c-3.46,-8.78 -11.99,-15 -22,-15Zm0,25c-5.52,0 -10,-4.48 -10,-10c0,-5.52 4.48,-10 10,-10c5.52,0 10,4.48 10,10c0,5.52 -4.48,10 -10,10Zm0,-16c-3.31,0 -6,2.69 -6,6c0,3.31 2.69,6 6,6c3.31,0 6,-2.69 6,-6c0,-3.31 -2.69,-6 -6,-6Z"></path>
</g>
</svg>
SVG store!
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
<symbol id="arrow" viewBox="0 0 16 15">
<path />
</symbol>
<symbol id="eye" viewBox="0 0 50 50">
...
</symbol>
</svg>
SVG store!
<svg class="bloko-icon">
<use xlink:href="#arrow" />
</svg>
<svg class="bloko-icon ...">
<use xlink:href="#eye" width="50" height="50" />
</svg>
module.exports = function(source) {
if (this.cacheable) {
this.cacheable();
}
const options = getOptions(this);
const template = fs.readFileSync(options.template, 'utf8');
return template.replace(
'<icon />',
source
.replace('{width}', 'width={width}')
.replace('{height}', 'height={height}')
.replace('class="{class}"', 'className={classNames}')
.replace('{fill}', 'fill={fill}')
.replace('{stroke}', 'stroke={stroke}');
);
};
module.exports = function(source) {
if (this.cacheable) {
this.cacheable();
}
const options = getOptions(this);
const template = fs.readFileSync(options.template, 'utf8');
const id = this.resourcePath.replace(/\//g, '-');
return template.replace(
'<icon />',
`<use
xlink:href="#${id}"
width={width}
height={height}
fill={fill}
stroke={stroke}
/>`
);
};
module.exports = function(source) {
if (this.cacheable) {
this.cacheable();
}
const options = getOptions(this);
const template = fs.readFileSync(options.template, 'utf8');
const id = this.resourcePath.replace(/\//g, '-');
fs.writeFileSync(`node_modules/our-loader/${this.resourcePath}`,
source.replace('{id}', id));
return template.replace(
'<icon />',
`<use
xlink:href="#${id}"
width={width}
height={height}
fill={fill}
stroke={stroke}
/>`
);
};
Это законно?
fs.writeFileSync(`node_modules/our-loader/${this.resourcePath}`,
source.replace('{id}', id));
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
<symbol id="arrow" viewBox="0 0 16 15">
<path />
</symbol>
<symbol id="eye" viewBox="0 0 50 50">
...
</symbol>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
<symbol id="arrow" viewBox="0 0 16 15">
<path />
</symbol>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
<symbol id="eye" viewBox="0 0 50 50">
...
</symbol>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
<symbol id="arrow" viewBox="0 0 16 15">
<path />
</symbol>
<symbol id="eye" viewBox="0 0 50 50">
...
</symbol>
<symbol id="arrow" viewBox="0 0 16 15">
<path />
</symbol>
<symbol id="eye" viewBox="0 0 50 50">
...
</symbol>
</svg>
(100 + чуть-чуть) * полкилобайта
(1 + чуть-чуть) * полкилобайта
(1 + чуть-чуть) * полкилобайта + 100 * 60 байт
(1 + чуть-чуть) * полкилобайта + 100 * 60 байт
6 Кб
≈60 Кб
6.5 Кб
VS
SVG Store
SVG Store
Lazy loading?
А как грузить?
Как грузить?
Как грузить?
Как грузить?
Как грузить?
Как грузить?
Как грузить?
Как грузить?
👍 Анимация
Как грузить?
👍 Анимация
👍 Кэш
Как грузить?
👍 Анимация
👍 Кэш
👍 Грузим только нужные иконки
Итого
Inline один из наиболее удобных способов, он позволяет нам решить практически все проблемы
Inline расширяем
Готовы ли вы
такое поддерживать?
Sprite
<svg width="124" height="16" viewBox="0 0 124 16"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<symbol id="filters" width="16" height="16">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1 4.667c0-.392.317-.709.708-.709h6a.708.708 0 1 1 0 1.417h-6A.708.708 0 0 1 1 4.667zM12.334 4.667c0-.392.317-.709.708-.709h1.333a.708.708 0 0 1 0 1.417h-1.333a.708.708 0 0 1-.708-.708z"/>
</symbol>
<use x="0" y="0" xlink:href="#filters" fill="#3d3e42"/>
<use x="18" y="0" xlink:href="#filters" fill="#fff"/>
</svg>
Sprite
CSS-Filters
<svg width="124" height="16" viewBox="0 0 124 16"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<symbol id="filters" width="16" height="16">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1 4.667c0-.392.317-.709.708-.709h6a.708.708 0 1 1 0 1.417h-6A.708.708 0 0 1 1 4.667zM12.334 4.667c0-.392.317-.709.708-.709h1.333a.708.708 0 0 1 0 1.417h-1.333a.708.708 0 0 1-.708-.708z"/>
</symbol>
<use x="0" y="0" xlink:href="#filters" fill="#3d3e42"/>
<use x="18" y="0" xlink:href="#filters" fill="#fff"/>
</svg>
.red {
filter: invert(30%) sepia(93%) saturate(7246%) hue-rotate(356deg)
brightness(102%) contrast(120%);
}
.blue {
filter: invert(10%) sepia(99%) saturate(4712%) hue-rotate(243deg)
brightness(108%) contrast(145%);
}
.green {
filter: invert(54%) sepia(48%) saturate(1714%) hue-rotate(79deg)
brightness(113%) contrast(126%);
}
Inline + Lazy Loading + SVG Store
Sprite
CSS-Filters
<svg width="124" height="16" viewBox="0 0 124 16"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<symbol id="filters" width="16" height="16">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1 4.667c0-.392.317-.709.708-.709h6a.708.708 0 1 1 0 1.417h-6A.708.708 0 0 1 1 4.667zM12.334 4.667c0-.392.317-.709.708-.709h1.333a.708.708 0 0 1 0 1.417h-1.333a.708.708 0 0 1-.708-.708z"/>
</symbol>
<use x="0" y="0" xlink:href="#filters" fill="#3d3e42"/>
<use x="18" y="0" xlink:href="#filters" fill="#fff"/>
</svg>
.red {
filter: invert(30%) sepia(93%) saturate(7246%) hue-rotate(356deg)
brightness(102%) contrast(120%);
}
.blue {
filter: invert(10%) sepia(99%) saturate(4712%) hue-rotate(243deg)
brightness(108%) contrast(145%);
}
.green {
filter: invert(54%) sepia(48%) saturate(1714%) hue-rotate(79deg)
brightness(113%) contrast(126%);
}
Inline + Lazy Loading + SVG Store
Sprite
CSS-Filters
CSS Masks
<svg width="124" height="16" viewBox="0 0 124 16"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<symbol id="filters" width="16" height="16">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1 4.667c0-.392.317-.709.708-.709h6a.708.708 0 1 1 0 1.417h-6A.708.708 0 0 1 1 4.667zM12.334 4.667c0-.392.317-.709.708-.709h1.333a.708.708 0 0 1 0 1.417h-1.333a.708.708 0 0 1-.708-.708z"/>
</symbol>
<use x="0" y="0" xlink:href="#filters" fill="#3d3e42"/>
<use x="18" y="0" xlink:href="#filters" fill="#fff"/>
</svg>
.red {
filter: invert(30%) sepia(93%) saturate(7246%) hue-rotate(356deg)
brightness(102%) contrast(120%);
}
.blue {
filter: invert(10%) sepia(99%) saturate(4712%) hue-rotate(243deg)
brightness(108%) contrast(145%);
}
.green {
filter: invert(54%) sepia(48%) saturate(1714%) hue-rotate(79deg)
brightness(113%) contrast(126%);
}
.clipped {
clip-path: polygon(50% 0%, 35% 63%, 82% 100%, 18% 100%, 0% 38%);
}
.masked {
mask: url(mask.svg);
}
Так как правильно? Какое решение правильно?
Все!
Не всегда решение покрывающее все минусы — подходящее
Экспериментируйте и "вовремя останавливайтесь"
Любое решение можно довести до высокой сложности
Вместо выводов
twitter: @xnimorz
Icon. Оверинжиниринг или необходимость?
By Nik Mostovoy
Icon. Оверинжиниринг или необходимость?
- 782