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 Gameface supports the following parts of the CSSTOM draft:

  • StylePropertyMap - With the following functions - set, get, delete, clear, has.
  • CSSStyleValue.parse
  • CSSKeywordValue
  • CSSUnitValue
  • CSSTransformValue with its CSSTransformComponent 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

  1. 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);
...
  1. StylePropertyMap contains the inline styles of the element. We get access to it by calling attributeStyleMap 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
  1. 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

  1. Not saving the attributeStyleMaps.
// 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);
    ...
}
  1. 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)]));
    ...
}
  1. 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:

  1. Save attributeStyleMaps to use set directly on the map instead of through element.attributeStyleMap
  2. Reuse CSSTOM objects by changing their value with what you want to update, instead of creating new ones
  3. Be wary of using 1 CSSUnitValue object for DIFFERENT CSS properties as they could have different internal representations which would slightly reduce performance.