CSS Typed Object Model
The CSS Typed Object Model is part of the Houdini APIs. It allows the manipulation of CSS styles through objects, reducing the number of strings and strings concatenations required. This can lead to improved performance, lower memory cost and less time spent on GC, especially when used with properties requiring lots of string concatenation like transform
.
Supported subset
Currently Prysm supports the following parts of the CSSTOM draft:
StylePropertyMap
- With the following functions -set
,get
,delete
,clear
,has
.CSSStyleValue.parse
CSSKeywordValue
CSSUnitValue
CSSTransformValue
with itsCSSTransformComponent
members:CSSTranslate
,CSSScale
,CSSRotate
,CSSSkewX
,CSSSkewY
.
Since most of the performance of using the CSSTOM is limited to transform
and the properties that use CSSUnitValue
, here is the list of properties that are supported and can be a CSSUnitValue
:
border-bottom-width
border-left-width
border-right-width
border-top-width
bottom
flex-basis
flex-grow
flex-shrink
font-size
height
left
letter-spacing
line-height
margin-bottom
margin-left
margin-right
margin-top
max-height
max-width
min-height
min-width
opacity
padding-bottom
padding-left
padding-right
padding-top
right
text-decoration-thickness
text-stroke-width
text-underline-offset
top
width
z-index
Shorthands and custom properties are NOT currently supported with CSSTOM.
Using the CSS Typed Object Model
- Example on CSSTOM usage with
CSSUnitValue
object that can be reused to set properties of multiple elements.
let width = CSSStyleValue.parse("width", "100px"); // Creates CSSUnitValue that internally is a parsed width
// This saves parsing on every set
...
width.value = newValue[0]; // Change only a number. No creating new string and string concatenation involved.
elementsAttributeStyleMaps[0].set("width", width); // We have saved the attributeStyleMaps previously.
// so we don't recreate them on every set
width.value = newValue[1]; // Reuse the object - only 1 per property is enough
elementsAttributeStyleMaps[1].set("width", width);
...
StylePropertyMap
contains the inline styles of the element. We get access to it by callingattributeStyleMap
on an element. So we can manipulate the inline styles in the following manner:
<div style="width: 100px;"> </div>
...
let attributeStyleMap = element.attributeStyleMap;
let width = attributeStyleMap.get("width");
console.log(width.value); // 100
console.log(width.unit); // "px"
attributeStyleMap.delete("width");
attributeStyleMap.has("width"); // false
attributeStyleMap.set("height", CSS.vw(100));
console.log(attributeStyleMap.get("height").value); // 100;
attributeStyleMap.clear();
attributeStyleMap.has("height"); // false
- The place where CSSTOM really shines is when you need to update a transformation of multiple elements on the screen very often. Here is an example of how to do that in a convenient and efficient manner with CSSTOM.
// This creates our `CSSTransform` object, parsing here is fine since we will do it only once
let transform = CSSStyleValue.parse("transform", "translate(20px, 100px) scale(1, 2) rotate(15deg)");
let elements = document.querySelectorAll(".ElementsThatTransformFrequently");
let attributeStyleMaps = [];
// Save the attributeStyleMaps that will get reused often for significantly improved performance
for(let i = 0; i < elements.length; i++)
attributeStyleMaps.push(elements[i].attributeStyleMap);
// updateValues contains just floats of the new values that we are going to set
function UpdateFunction(updateValues) {
for(let i = 0; i < attributeStyleMaps.length; i++) {
// This would require 10 string concatenations if done with regular CSSOM
transform[0].x.value = updateValues[i][0];
transform[0].y.value = updateValues[i][1];
transform[1].x.value = updateValues[i][2];
transform[1].y.value = updateValues[i][3];
transform[2].angle.value = updateValues[i][4];
attributeStyleMaps[i].set("transform", transform);
}
}
Common pitfalls when using CSS Typed Object Model
- Not saving the
attributeStyleMap
s.
// Prefer
let attributeStyleMaps = [];
for(let i = 0; i < elements.length; i++)
attributeStyleMaps.push(elements[i].attributeStyleMap);
...
function UpdateStylesOften() {
...
attributeStyleMaps[i].set("someProp", somePropValue);
...
}
// AVOID
function UpdateStylesOften() {
...
elements[i].attributeStyleMaps.set("someProp", somePropValue);
...
}
- Creating new CSSTOM objects on every update, instead of reusing CSSTOM objects and only setting their values
// AVOID
function UpdateStylesOften() {
...
elementMaps[i].set("top", CSS.px(150));
elementMaps[i].set("left", CSS.px(50));
elementMaps[i].set("transform", new CSSTransformValue([new CSSScale(2,3)]));
...
}
- Having 1 CSSTOM object for DIFFERENT properties.
// AVOID
let unitValueObj = CSSStyleValue.parse("width", CSS.px(50));
...
function UpdateStylesOften() {
...
unitValueObj.value = 20;
elementMaps[i].set("left-padding", unitValueObj);
unitValueObj.value = 10;
elementMaps[i].set("width", unitValueObj);
...
}
Doing this will not allow us to use the already existing internal representation of “width” value for “left-padding” since internally they have different representation. It is better to have a separate CSSTOM object for every different CSS property that will need frequent updating. Since there aren’t that many properties representable by CSSUnitValue
this should be a negligible memory overhead.
// Prefer
let widthObj = CSSStyleValue.parse("width", CSS.px(50));
let leftPadObj = CSSStyleValue.parse("left-padding", CSS.px(50));
...
function UpdateStylesOften() {
...
leftPadObj.value = 20;
elementMaps[i].set("left-padding", leftPadObj);
widthObj.value = 10;
elementMaps[i].set("width", widthObj);
...
}
So overall for properties that will get updated often:
- Save
attributeStyleMap
s to use set directly on the map instead of throughelement.attributeStyleMap
- Reuse CSSTOM objects by changing their value with what you want to update, instead of creating new ones
- Be wary of using 1
CSSUnitValue
object for DIFFERENT CSS properties as they could have different internal representations which would slightly reduce performance.