Houdini — CSS, который JavaScript

Никита Дубко

Никита Дубко

Houdini —
CSS, который JavaScript

Никита Дубко

MinskCSS MinskJS CSS-Minsk-JS Никита Дубко

Конвейер рендеринга в браузере

Inside look at modern web browser (part 3), Mariko Kosaka

Конвейер рендеринга в браузере

  1. Скачивание и парсинг HTML, CSS и JavaScript

Конвейер рендеринга в браузере

  1. Скачивание и парсинг HTML, CSS и JavaScript
  2. Построение Document Object Model и CSS Object Model

Конвейер рендеринга в браузере

  1. Скачивание и парсинг HTML, CSS и JavaScript
  2. Построение Document Object Model и CSS Object Model
  3. Формирование дерева рендеринга

Конвейер рендеринга в браузере

  1. Скачивание и парсинг HTML, CSS и JavaScript
  2. Построение Document Object Model и CSS Object Model
  3. Формирование дерева рендеринга
  4. Расчет положения на странице каждого элемента дерева рендеринга

Конвейер рендеринга в браузере

  1. Скачивание и парсинг HTML, CSS и JavaScript
  2. Построение Document Object Model и CSS Object Model
  3. Формирование дерева рендеринга
  4. Расчет положения на странице каждого элемента дерева рендеринга
  5. Отрисовка пикселей каждого слоя

Конвейер рендеринга в браузере

  1. Скачивание и парсинг HTML, CSS и JavaScript
  2. Построение Document Object Model и CSS Object Model
  3. Формирование дерева рендеринга
  4. Расчет положения на странице каждого элемента дерева рендеринга
  5. Отрисовка пикселей каждого слоя
  6. Компоновка слоев и отображение в видимой области браузера

Конвейер рендеринга в браузере

  1. Магия 🔮
  2. Можем влиять на процесс из JavaScript 💪
  3. Магия 🔮
  4. Магия 🔮
  5. Магия 🔮
  6. Магия 🔮

JavaScript

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

Полифил

(function(self) {
    'use strict';
    if (self.es2020feature) {
        return;
    }
    self.es2020feature = function() {
	      // Do some magic
    };
    self.es2020feature.polyfill = true;
})(this);

Понифил 🦄

ponyfill.com

// is-nan-ponyfill.js
module.exports = function (value) {
	return value !== value;
};

// app.js
var isNanPonyfill = require('is-nan-ponyfill');
isNanPonyfill(5);

Транспайлер

// ES6
let point = { x: 0, y: 0 };
const {x, y} = point;
const arrowFunction = (a) => a ** 2;
// ES5
'use strict';
var point = { x: 0, y: 0 };
var x = point.x,
    y = point.y;
var arrowFunction = function arrowFunction(a) {
  return Math.pow(a, 2);
};

CSS

Ошибки в проектировании CSS

LESS SASS Stylus PostCSS

@supports 😍

.articles {
	display: grid;
	grid-template-columns: repeat(4, 1fr);
}

@supports (display: layout(magic)) {
	.articles {
		--masonry-columns-count: 4;
		display: layout(masonry);
	}
}

Polyfill.js 🎉

Polyfill({
		declarations:["position:sticky"]
	})
	.doMatched(function(rules) {
		/* add styles */
	 })
	.undoUnmatched(function(rules) {
		/* reset styles */
	});
Polyfill.js author's message
Sad

CSS-TAG Houdini Task Force 💎

CSS-TAG Houdini Task Force
Гарри Гудини
Новые спецификации Houdini
Новые спецификации Houdini для работы с конвейером рендеринга
CSS Houdini specifications

Репозиторий проекта на Github

CSS Custom Properties

:root {
	--r: 255;
	--g: 20;
	--b: 147;
	--primary-color: rgb(var(--r), var(--g), var(--b));
}

.my-element {
	--e-width: 200px;
	--e-border-width: 20px;
	background-color: var(--primary-color);
	width: calc(var(--e-width) + var(--e-border-width));
}
const isSupported = CSS.supports('--custom', 'property');
// Получить значение custom property
const pinkElement = document.querySelector('.pink-element');
const elementWidth = window
   .getComputedStyle(pinkElement)
   .getPropertyValue('--element-width');
// Изменить custom property для узла
pinkElement.style.setProperty('--element-width', '300px');
// Изменить custom property в :root
document.documentElement.style.setProperty('--r', 190);
Custom Properties — caniuse.com

JavaScript
↕️
Custom Properties
↕️
CSS

// JS
window.addEventListener('mousemove', function(e) {
	const target = document.documentElement;
	target.style.setProperty('--x', e.clientX);
	target.style.setProperty('--y', e.clientY);
});

/* CSS */
.element {
	transform: translate(
		calc(var(--x) / 10 * 1px),
		calc(var(--y) / 10 * 1px)
	);
}

демо Stargate

--foo: if(x > 5) this.width = 10; ✅

chrome://flags

Experimental flag

CSS Typed OM

Проблема #1

const fontSize = window
	.getComputedStyle(elem)
	.getPropertyValue('font-size');
const fontSizeValue = parseFloat(fontSize);
console.log(fontSizeValue); // 25

Решение #1

const element = document.querySelector('.elem');

const styleMap = element.attributeStyleMap;
console.log( styleMap.get('font-size') );
// CSSUnitValue {value: 2.5, unit: "em"}
const computedStyleMap = element.computedStyleMap();
console.log( computedStyleMap.get('font-size') );
// CSSUnitValue {value: 25, unit: "px"}

Проблема #2

const x = 50;
const offset = 10;
element.style.setProperty(
	'transform',
	`translate(${x}px, calc(1em + ${offset}px))`
);

Решение #2

const styleMap = element.attributeStyleMap;
const transformValue = new CSSTransformValue([
	new CSSTranslate(
		CSS.px(50),
		new CSSMathSum(CSS.em(1), CSS.px(5))
	)
]);
styleMap.set('transform', transformValue);

Отлов ошибок

try {
	CSSStyleValue.parse(
		'transform',
		'translate4d(bogus value)'
	);
} catch (err) {
	console.err(err);
}

Иерархия возможных значений свойств

Проверить поддержку

if (window.CSS && CSS.number) {
	// В вашем браузере CSS Typed OM работает
}
		

полифил typed-om

полифил typed-om

Chrome 66+

Где почитать?

CSS Parser API

Идея

const background = window.cssParse.rule("background: green");
background.attributeStyleMap.get("background").value; // "green"

const styles = window.cssParse.ruleSet(
	`.foo {
		background: green;
		margin: 42px;
	}`
);
styles.length; // 5
styles[0].attributeStyleMap.get("margin-top").value; // 42

Асинхронная загрузка

const style = fetch("style.css")
	.then(response => {
		const styles = CSS.parseStylesheet(response.body);
		// применить PostCSS прямо в браузере
		return styles;
	});
style.then(console.log);

CSSStyleSheet

const sheet = new CSSStyleSheet();
sheet.replaceSync('a { color: red; }');

sheet.replace('@import url("styles.css")')
	.then(sheet => {
		console.log('Styles loaded successfully');
	})
	.catch(err => {
		console.error('Failed to load:', err);
	});
developers.google.com/web/updates/2019/03/nic73

CSS Properties and Values API

Проблема #3

:root {
	--sidebar-width: 400px;
}
.closed {
	--sidebar-width: 80px;
}
body {
	transition: --sidebar-width 1s;
}
// JavaScript
CSS.registerProperty({
	name: '--sidebar-width',
	syntax: '<length>',
	inherits: true,
	initialValue: '80px'
});
/* CSS */
@property --sidebar-width {
	syntax: "<length>";
	inherits: true;
	initial-value: 80px;
}

Синтаксис

<length>
<number>
<percentage>
<length-percentage>
<color>
<image>
<url>
<integer>
<angle>
<time>
<resolution>
<transform-function>
<custom-ident>

<length> | <number>
<length>+
<image>#
small | smaller

Демо Mebius

Chrome Canary *

Observer github.com/w3c/css-houdini-drafts/issues/555

Worklets

// app.js — псевдокод
window.someWorklet
	.addModule('some-worklet.js')
	.then(_ => {
		console.log('some-worklet — loaded');
	});

// some-worklet.js — псевдокод
registerSomeWorklet('some-worklet-name', class {
	process(arg) {
		// делаем магию
	}
});
Много потоков

CSS Painting API 🎨

Демо image-placeholder

CSS Painting API

Можно применять для отрисовки свойств:

registerPaint('my-paint', class MyPaint {
	static get inputProperties() { return ['--foo']; }
	static get inputArguments() { return ['<color>']; }
	static get contextOptions() { return { alpha: true }; }

	paint(ctx, geom, properties, args) {
		// Можно рисовать почти как на обычном canvas
	}
});
/* style.css */
.my-element {
	--foo: deeppink;
	background-image: paint(my-paint);
}

// app.js
CSS.paintWorklet.addModule('my-paint.js');

Ограничения контекста для Paint Worklet

Chrome 65+

css-paint-polyfill

Box Tree API



<style>
p::first-line { color: green; }
p::first-letter { color: red; }
</style>

<p>foo <i>bar baz</i></p>

foo bar baz

const element = document.querySelector('.my-element');
element
	.getFragmentInformation("direct-fragments-only")
	.then(info => {
		// ...
	});

// Значения фильтра
"direct-fragments-only" | "fragment-hierarchy"
interface DeadFragmentInformation {
	Node node;
	double width;
	double height;
	double top;
	double left;
	boolean isOverflowed;
	FrozenArray<DeadFragmentInformation>? children;
	DeadFragmentInformation? nextSibling;
	DeadFragmentInformation? previousSibling;
	DeadFragmentInformation? nextInBox;
	DeadFragmentInformation? previousInBox;
};

CSS Layout API 🚀

Masonry

Демо masonry

registerLayout('example', class {
	static inputProperties = ['--foo'];
	static childrenInputProperties = ['--bar'];
	static layoutOptions = {
		childDisplay: 'normal',
		sizing: 'manual'
	};
	async intrinsicSizes(children, edges, styleMap) {
		// Intrinsic sizes code goes here.
	}
	async layout(children, edges, constraints, styleMap, breakToken) {
		// Layout code goes here.
	}
});
/* CSS */
.layout-example {
	--foo: 5;
	display: layout(my-layout);
}

.layout-example .child {
	--bar: 200px;
}
// JavaScript
window.layoutWorklet.addModule('my-layout.js');
Layout Edges

Идеи

Promise

CSS Animation Worklet API

Демо parallax-animation

Web Animations

// JavaScript
const element = document.querySelector('.my-element');
element.animate([
	{'--some-color': 'red',  'opacity': 0 },
	{'--some-color': 'blue', 'opacity': 1 },
], {
	direction: 'alternate',
	duration: 5000,
	iterations: Infinity,
});

Polyfill

Web Animations
// animator.js
registerAnimator(
	'scroll-position-animator',
	class {
		constructor(options = {}) {
			this.coef = options.coef || 1;
		}

		animate(currentTime, effect) {
			effect.localTime = currentTime * this.coef;
		}
	});
Houdini's Animation Worklet, Surma
// app.js
await CSS.animationWorklet.addModule("animator.js");

new WorkletAnimation(
	'scroll-position-animator',
	keyframeEffect,
	scrollTimeline,
	{ coef: 1.2 }
).play();
const keyframeEffect = new KeyframeEffect(
	document.querySelector('#target'),
	[
		{ transform: 'translateX(0)' },
		{ transform: 'translateX(500px)' }
	],
	{
		duration: 2000,
		iterations: Number.POSITIVE_INFINITY
	}
);
const scrollTimeline = new ScrollTimeline({
	timeRange: 2000,
	scrollSource: document.querySelector('.source'),
	orientation: 'vertical',
	startScrollOffset: '200px',
	endScrollOffset: '500px'
});

Полифил anim-worklet

Демо scroller

Демо reading-progress

Где использовать?

Strength

Font Metrics API

Метрики шрифтов
const element = document.querySelector('.my-element');
document.measureElement(element);
interface FontMetrics {
	double width;
	FrozenArray<double> advances;
	double boundingBoxLeft;
	double boundingBoxRight;
	double height;
	double emHeightAscent;
	double emHeightDescent;
	double boundingBoxAscent;
	double boundingBoxDescent;
	double fontBoundingBoxAscent;
	double fontBoundingBoxDescent;
	Baseline dominantBaseline;
	FrozenArray<Baseline> baselines;
	FrozenArray<Font> fonts;
};
Happy

Почему Houdini хорош?

👍

Недостатки Houdini

🥺
https://ishoudinireadyyet.com

Is Houdini ready yet‽

DasSurma

houdini.glitch.me

Где вдохновляться?

JS-in-CSS

Материалы по теме

Статьи

  1. Interactive Introduction to CSS Houdini, Sam Richard
  2. Houdini: Maybe The Most Exciting Development In CSS You’ve Never Heard Of, Philip Walton
  3. What Houdini Means for Animating Transforms, Ana Tudor
  4. The CSS Paint API, Ruth John

Видео

  1. Serg Hospodarets: CSS Houdini - from CSS variables to JavaScript and back — Frontend United 2017
  2. Patrick Kettner — Creating magic with Houdini — OdessaJS 2018
  3. Surma — State of Houdini — Chrome Dev Summit 2018

Спасибо за внимание!

@dark_mefody
n.a.dubko@gmail.com

QR code — ссылка на презентацию
Parser DOM / CSSOM Cascade Layout Paint Composite