CSS Houdini: CSS Typed Object Model

If you work with CSS in JS you will soon discover that all values CSS gives the JS parser are strings that you must parse to get the data you need to continue processing the styles. This is when you use the element.style.attribute syntax.

// Element styles.
el.style.opacity = 0.3;
typeof el.style.opacity === 'string' // Ugh. A string!?

// Stylesheet rules.
document.styleSheets[0].cssRules[0].style.opacity = 0.3;

The CSS Typed Object Model (Typed OM) adds types, methods, and an object model to CSS values. Instead of strings, values are exposed as JavaScript objects to facilitate performant (and sane) manipulation of CSS.

Instead of using element.style, you’ll be accessing styles through a new .attributeStyleMap property for elements and a .styleMap property for stylesheet rules. Both return a StylePropertyMap object.

The functionality of the Stylesheet version of the

// Element styles.
// Set element style
el.attributeStyleMap.set('opacity', 0.3);
// Get the element style's value
el.attributeStyleMap.get('opacity').value;
// list all style attributes for element
console.log(el.attributeStyleMap)
// What kind of element is the value for the element?
typeof el.attributeStyleMap.get('opacity').value

// Stylesheet rules.
const stylesheet = document.styleSheets[0];
stylesheet.cssRules[0].styleMap.set('background', 'blue');

So, why should we care?

  • Numerical values are always returned as numbers, not strings, so you do less work and get consistent results
  • Value clamping & rounding. Typed OM rounds and/or clamps values so they’re within the acceptable ranges for a property
  • Better performance. The browser has to do less work serializing and deserializing string values
  • We can use Javascript error handling for our CSS
  • CSS property names in Typed OM are always strings, matching what you actually write in CSS

Basic Usage: Feature Detection

I’m always one to code defensively. To make sure that the browser supports CSS Typed OM we can use something like the code below to make sure that the browser will work with the code we want to use. If it doesn’t then we can fall back to using the current object model (el.style.attribute).

if (window.CSS && CSS.number) {
  // Work with CSS Typed OM.
  el.attributeStyleMap.set('opacity', 0.3);
} else {
  el.style.opacity = '0.3';
}

Setting Single Value Attributes

The basic thing to do is to set and retrieve values. Using the attributeStyleMap to set up the values for attributes.

Typed OM provides convenient methods to make it easier to create typed values without having to use strings. Note that some of these values are part of specifications currently under development and may have little or no support among current browsers.

The values in the current Typed OM specification are:

  • Length
    • CSS.em();
    • CSS.ex();
    • CSS.ch();
    • CSS.ic();
    • CSS.rem();
    • CSS.lh();
    • CSS.rlh();
    • CSS.vw();
    • CSS.vh();
    • CSS.vi();
    • CSS.vb();
    • CSS.vmin();
    • CSS.vmax();
    • CSS.cm();
    • CSS.mm();
    • CSS.Q();
    • CSS.in();
    • CSS.pt();
    • CSS.pc();
    • CSS.px();
  • angle
    • CSS.deg();
    • CSS.grad();
    • CSS.rad();
    • CSS.turn();
  • time
    • CSS.s();
    • CSS.ms();
  • frequency
    • CSS.Hz();
    • CSS.kHz();
  • resolution
    • CSS.dpi();
    • CSS.dpcm();
    • CSS.dppx();
  • flex
    • CSS.fr();

For more information about the units listed above check the CSS Values and Units Level 4 specification.

You can also use strings. The parser will convert it to numbers under the hood. The two declarations below are equivalent.

el.attributeStyleMap.set('margin-top', CSS.px(10));
el.attributeStyleMap.set('margin-top', '10px');

Complex Numeric Values

There are times when you need to represent more than a single value, for example when doing something like calc(1em + 10px).

We can get more complex values by using mathematical functions that are part of the Typed OM. The Spec makes the following arithmetic functions available that will return either a typed value or a calc() function depending on the parameters passed.

  • add();
  • sub();
  • mul();
  • div();
  • min();
  • max();
// Multiply 2 values, keep the unit
CSS.deg(90).mul(2)
// {value: 180, unit: "deg"}

// Pick the bigger of the two values
CSS.percent(50).max(CSS.vw(50)).toString()
// "max(50%, 50vw)"

// Add to numbers of the same unit
CSS.px(1).add(CSS.px(2))
// {value: 3, unit: "px"}

// multiple values:
CSS.s(1).sub(CSS.ms(200), CSS.ms(300)).toString()
// "calc(1s + -200ms + -300ms)"

// or pass a `CSSMathSum`:
const sum = new CSSMathSum(CSS.percent(100), CSS.px(20)));
CSS.vw(100).add(sum).toString() // "calc(100vw + (100% + 20px))"

CSS Math Values

The values created by cssMathValue objects contain one or more mathematical expressions. Notice how we use the toString() method to explicitly convert the values to strings that we can feedback to CSS for it to work consistently.

Also, note that unless we usually get a calc() function that we can drop into the stylesheets to continue manipulating or leave in the stylesheet as the final value.

new CSSMathSum(CSS.vw(100), CSS.px(-10)).toString();
// "calc(100vw + -10px)"

new CSSMathNegate(CSS.px(42)).toString()
// "calc(-42px)"

new CSSMathInvert(CSS.s(10)).toString()
// "calc(1 / 10s)"

new CSSMathProduct(CSS.deg(90), CSS.number(Math.PI/180)).toString();
// "calc(90deg * 0.0174533)"

new CSSMathMin(CSS.percent(80), CSS.px(12)).toString();
// "min(80%, 12px)"

new CSSMathMax(CSS.percent(80), CSS.px(12)).toString();
// "max(80%, 12px)"

Keyword Values

There are values that are not represented by a number + unit combination, like the values for display or inherited. Use CSSKeyWordValue in those cases.

el.attributeStyleMap.set('display', new CSSKeywordValue('initial'));
el.attributeStyleMap.get('display').value // 'initial'
el.attributeStyleMap.get('display').unit // undefined

Retrieving values: attributeStyleMap versus computedStyleMap

We can then retrieve the value (without a unit) or the unit itself.

el.attributeStyleMap.get('margin-top').value  // 10
el.attributeStyleMap.get('margin-top').unit // 'px'

There are situations where the element may have more than a single value like background-image. In this situation, get will only give us the first value for the attribute. Fortunately, we have a getAll method that would return all the values in a map.

el.attributeStyleMap.getAll('background-image')

Computed Styles versus Attribute Styles

attributeStyleMap gives you the value you entered but this presents one interesting case for me. What happens when we give it a percentage or other relative values like em or rem?

We can use different units defined outside the Typed OM like window.getComputedStyle() to get the resulting value of a property after all parent object values are calculated.

The difference between window.getComputedStyle() (outside the Typed OM) and element.computedStyleMap() (part of the Typed OM) is that the window.getComputedStyle() method gets the value after all calculations have been done. If we need to get the value as we’re working with it then we can use the element.computedStyleMap() method.

CSS Transforms

CSS Transforms are created with aCSSTransformValue objects passing an array of transform values (e.g. CSSRotate, CSScale, CSSSkew, CSSSkewX, CSSSkewY). As an example, say you want to re-create this CSS:

transform: rotateZ(45deg) scale(0.5) translate3d(10px,10px,10px);

Translated into Typed OM:

const transform =  new CSSTransformValue([
  new CSSRotate(CSS.deg(45)),
  new CSSScale(CSS.number(0.5), CSS.number(0.5)),
  new CSSTranslate(CSS.px(10), CSS.px(10), CSS.px(10))
]);

In addition to its verbosity (lolz!), CSSTransformValue has some cool features. It has a boolean property to differentiate 2D and 3D transforms and a .toMatrix() method to return the DOMMatrix representation of a transform:

new CSSTranslate(CSS.px(10), CSS.px(10)).is2D // true
new CSSTranslate(CSS.px(10), CSS.px(10), CSS.px(10)).is2D // false
new CSSTranslate(CSS.px(10), CSS.px(10)).toMatrix() // DOMMatrix

Custom Properties

CSS var() become a CSSVariableReferenceValue object in the Typed OM. Their values get parsed into CSSUnparsedValue because they can take any type (px, %, em, rgba(), etc).

const foo = new CSSVariableReferenceValue('--foo');
// foo.variable === '--foo'

// Fallback values:
const padding = new CSSVariableReferenceValue(
    '--default-padding', new CSSUnparsedValue(['8px']));

If you want to get the value of a custom property, there’s a bit of work to do:

<style>
  body {
    --foo: 10px;
  }
</style>
<script>
  const styles = document.querySelector('style');
  const foo = styles.sheet.cssRules[0].styleMap.get('--foo').trim();
  console.log(CSSNumericValue.parse(foo).value); // 10
</script>

CSS Positions

CSSPositionValue objects represent a space-separated list of values, like those used by object-position.

const position = new CSSPositionValue(CSS.px(5), CSS.px(10));
el.attributeStyleMap.set('object-position', position);

console.log(position.x.value, position.y.value);
// → 5, 10

CSS Images

CSSImageValue objects represent one or more values for images, like those used in background-image, list-style-image, and border-image-source. As we discussed earlier these elements may return one or more images as the value so we need to retrieve them with element.attributeStyleMap.getAll() to make sure we capture all the images used in the element.

Parsing values

The Typed OM introduces parsing methods to the web platform! This means you can finally parse CSS values programmatically, before trying to use it! This new capability is a potential lifesaver for catching early bugs and malformed CSS.

Parse a full style:

const css = CSSStyleValue.parse(
    'transform', 'translate3d(10px,10px,0) scale(0.5)');
// → css instanceof CSSTransformValue === true
// → css.toString() === 'translate3d(10px, 10px, 0) scale(0.5)'

Parse values into CSSUnitValue:

CSSNumericValue.parse('42.0px') // {value: 42, unit: 'px'}

// But it's easier to use the factory functions:
CSS.px(42.0) // '42px'

Error handling

Because we’re working with Javascript to

try {
  const css = CSSStyleValue.parse('transform', 'translate4d(bogus value)');
  // use css
} catch (err) {
  console.err(err);
}

Links and Resources

CSS Houdini: Present and Future of CSS

If we grow the language in these few ways [that allow extensibility], then we will not need to grow it in a hundred other ways; the users can take on the rest of the task.

Guy Steele, Growing a Language, 1998 ACM OOPSLA

CSS Houdini is a set of APIs under development by the CSS Working Group and the Technical Architecture Group to provide users with ways to extend CSS to match their needs. The extensibility language is usually JavaScript but some of these are written in pure CSS.

To translate the tech speak: Houdini will provide the hooks for developers to create their own content types in a performant way without having to wait for the working group to create specs for and browsers to implement.

Tab Atkins from the Chrome team presented about Houdini at CSS Day 2017. He does a much better job at explaining the APIs (expected since he’s one of the editors).

Surma, from the Chrome team, created is Houdini ready yet? as a way to track what Houdini is working on and the status of these different projects. I’ve grouped the table into three categories for the list below:

Houdini APIs rely on Worklets, a separate specification. This spec provides the underpinnings for the other Houdini specifications.

Before we get started, it’s important to know that this is not CSS in JS! It’s a way to enhance CSS with Javascript to produce results that are impossible to produce with CSS alone, as it exists today.

Worklets

Worklets are lightweight web workers that are better suited than the heavier workers for the kinds of work Houdini is intended for. You will see several different kinds of worklets that are specific for each API.

Prototypal Inheritance and Classes

If you’re working with modern evergreen browsers, classes are the preferred way to create reusable code. They work in all modern browsers according to caniuse.com out of the box. Prototypal inheritance will work if you need to support older browsers.

Prototypal Inheritance

Before ES6 we created reusable content using functions to create objects and attaching the methods to the object’s prototype. This way every object that inherits from the object will also inherit the methods attached to the prototype.

In the example below the Person has the following properties: first, last, age, gender, and interests. all these properties are required for all Person objects.

It also defines two methods for the Person object with the syntax Person.prototype.methodName to create a function representing the method.

Both methods return alerts with strings that interpolate values using string concatenation to pull elements from the Person object to display it.

The interests attribute is an array of one or more elements. For these examples, I’ve made them arrays of one item.

The name attribute is an object made of two children: first and last. This is why the methods refer to this.name.first rather than this.first.

Be careful when using string interpolation that you match the quotation marks and that you escape quotation marks that are part of the sentence rather than the string. Look at how we escaped the apostrophe in I'm to make sure that it didn’t close the string and caused errors when we run the code.

function Person(first, last, age, gender, interests) {
  this.name = {
    first,
    last
  };
  this.age = age;
  this.gender = gender;
  this.interests = interests;
};

//
Person.prototype.greeting = function() {
  alert('Hi! I\'m ' + this.name.first + '.');
};

Person.prototype.farewell = function() {
  alert(this.name.first + ' has left the building. Bye for now!');
}

We can then create new People based on the Person object with their four attributes and getting the two methods we defined. We use the object.method syntax to use the methods defined in the object’s prototype.

const kirk = new Person('Jim', 'Kirk', 39, 'male', ['starships']);
const mccoy = new Person('Leonard', 'McCoy', 44 ['Medicine']);

kirk.greeting();
// Hi! I'm Jim.
mccoy.farewell();
// Leonard has left the building. Bye for now!

ES2015 Classes

ES2015 introduces classes to JavaScript as a way to give a cleaner syntax for reusable code. This syntax will be familiar if you’ve written code in C++ and Java. We’ll convert the Person and Teacher examples from prototypal inheritance to a Class.

The constructor element builds the function that represents our class, in our example Person. All person objects will have these items.

greeting() and farewell() are class methods. They are defined as part of the class but are not required like the items inside the constructor. We can have a person without greeting and farewell but we can not have a farewell or a greeting without a person.

Both class methods use string literals rather than string concatenation in the class methods to make string interpolation easier and the code easier to read.

class Person {
  constructor(first, last, age, gender, interests) {

    this.name = {
      first,
      last
    };

    this.age = age;
    this.gender = gender;
    this.interests = interests;
  }


  greeting() {
    alert(`Hi! I'm ${this.name.first}`);
  };

  farewell() {
    alert(`${this.name.first} has left the building. Bye for now!`);
  };
}

We then instantiate Person objects using the new Person() syntax and call methods using the object.method syntax.

let han = new Person('Han', 'Solo', 25, 'male', ['smuggling']);
han.greeting();
// Hi! I'm Han

let leia = new Person('Leia', 'Organa', 19, 'female' ['government']]);
leia.farewell();
// Leia has left the building. Bye for now

Under the hood, JavaScript engines will convert the classes into Prototypal Inheritance models. You don’t have to worry about it, it’s all done for you.

Inheritance: Creating Classes Based On Other Classes

We created a class to represent a Person. They have a series of attributes that are common to all people; Let’s say that we want to create a specialized type of Person: a Teacher.

To save ourselves the typing we’ll base our teacher class on the person we’ve already created. This is called creating a subclass or subclassing.

To create a subclass we use the extend keyword to tell Javascript the class we want to base our class on.

class Teacher extends Person {
  constructor(first, last, age, gender, interests, subject, grade) {
    this.name = {
      first,
      last
    };

  this.age = age;
  this.gender = gender;
  this.interests = interests;
  // subject and grade are specific to Teacher
  this.subject = subject;
  this.grade = grade;
  }
}

We can make the code more readable by using the super() declaration as the first item in the class constructor. This will call the parent class’ constructor to take care of items defined there. We can further refine the Teacher example by calling super in the constructor; this will call Person’s constructor and set up its values.

We can also add additional items that are specific to the class we are creating like subject and grade in the Teacher example.

class Teacher extends Person {
  constructor(first, last, age, gender, interests, subject, grade) {
    super(first, last, age, gender, interests);

    // subject and grade are specific to Teacher
    this.subject = subject;
    this.grade = grade;
  }
}

The teacher class uses values from Person and adds functionality that goes beyond the parent class. We didn’t write greeting() and farewell() methods for Teacher but they are available as part of Person, so we can still use them, we get them “for free” in Teacher objects.

let snape = new Teacher('Severus', 'Snape', 58, 'male', ['potions'], 'dark arts', 5);
snape.greeting(); // Hi! I'm Severus.
snape.farewell(); // Severus has left the building. Bye for now.

Like we did with Teachers, we could create other subclasses of person to make them more specialized without modifying the base person class.

Getters and Setters

There may be times when we want to change the values of an attribute in the classes we create or we don’t know what the final value of an attribute will be. Using the Teacher example, we may not know what subject the teacher teaches or the subject may change from the time we create the teacher object to the time we use it.

We can do it with getters and setters.

We’ll use the Teacher class we defined earlier and enhance it with getters and setters. The class starts the same than it was the last time we looked at it.

Getters and setters work in pairs. Getters return the current value of the variable and setters change the value of the variable to the ones it defines.

The modified Teacher class looks like this:

class Teacher extends Person {
  constructor(first, last, age, gender, interests, subject, grade) {
    super(first, last, age, gender, interests);
    // subject and grade are specific to Teacher
    this._subject = subject;
    this.grade = grade;
  }

  get subject() {
    return this._subject;
  }

  set subject(newSubject) {
    this._subject = newSubject;
  }
}

In our class above we have a getter and setter for the subject property. We use _  to create a backing field to store our name property. Without this convention, we will get stack overflow errors every time we call get or set.

To show the current value of the _subject property of the snape object use snape._subject. To assign a new value to the _subject property use snape._subject="new value". The example below shows the two features in action:

// Check the default value
snape._subject // Returns "potions"

// Change the value
snape._subject="magic arts"
// Sets subject to "magic arts"

// Check it again and see if it matches the new value
snape._subject // Returns "magic arts"

AMP: Hope and Fears (Part 3)

Conclusion

I wrote the tweet below as a response to my original tweet.

As I’ve mentioned throughout this post the technology behind AMP is not revolutionary. It’s a reaction to a problem on the mobile web and it has been taken as a way to gate keep and restrict what can be done on the web.

There are many unanswered questions about AMP and the direction it’s going on for me to be comfortable with it. AMP email and Stories are out, what comes next?

Attempting to make AMP the canonical version of your web content worries me, it means we’re moving away from having an HTML version altogether and people may choose to learn AMP but not HTML (sorry, but HTML is the spec that lives at WHATWG and W3C and pages built from that spec, not AMP) and when AMP limitations become too restrictive will not know how to build web content that doesn’t require AMP and be starting from square one again.

It is tempting to draw comparisons between AMP and jQuery but the web was different when jQuery was first introduced. The use cases jQuery dealt with were about smoothing out different ways browsers did things (looking at you, Netscape and IE) and making sure you wouldn’t have to write similar code 3 times (one for IE, one for Netscape and one for browsers that supported standards when those became available) but now the technology is available to make sure we do things right, it’s a matter of whether we choose to use them wisely or not.

I mentioned earlier but having AMP Stories be completely different to any non-amp alternatives makes me worry as much if not more than making AMP the canonical version of my content. If there is no relation between my web content and the AMP stories that are built around it then what’s going to be the content that gets returned by the Googlebot when you search for it? I know this is not, strictly, an AMP issue and more of a search issue but since it’s an AMP product it’s worth pointing out here.

I’ll keep my mind open towards AMP (after all I still provide AMP for my blog content to help people with slow connections or who may have data cost issues) but there are too many open questions about the platform and its direction before I can say I agree with their assessment of how they want to move the web forward.

Links and Resources

AMP: Hope and Fears (Part 2)

Technical questions

There are technical questions that came up when researching AMP and writing this post a few more technical questions

Converting your content?

In talking to people at the Roadshow I now understand that AMP is a testbed to make things better and then bring it back to the wider web community; this is an important goal. I just have issues with the way AMP is doing this.

The example below is a basic sample HTML document conforming to the AMP HTML specification.

<!doctype html>
<html amp>
  <head>
    <meta charset="utf-8">
    <title>Sample document</title>
    <link rel="canonical" href="./regular-html-version.html">
    <meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">
    <style amp-custom>
      h1 {color: red}
    </style>
    <script type="application/ld+json">
    {
      "@context": "http://schema.org",
      "@type": "NewsArticle",
      "headline": "Article headline",
      "image": [
        "thumbnail1.jpg"
      ],
      "datePublished": "2015-02-05T08:00:00+08:00"
    }
    </script>
    <script async custom-element="amp-carousel" src="https://cdn.ampproject.org/v0/amp-carousel-0.1.js"></script>
    <style amp-boilerplate>body{-webkit-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-moz-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-ms-animation:-amp-start 8s steps(1,end) 0s 1 normal both;animation:-amp-start 8s steps(1,end) 0s 1 normal both}@-webkit-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-moz-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-ms-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-o-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}</style><noscript><style amp-boilerplate>body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}</style></noscript>
    <script async src="https://cdn.ampproject.org/v0.js"></script>
  </head>
  <body>
    <!-- Your amp-approved content goes here -->
  </body>
</html>

Some of the requirements and limitations of AMP are not evident. According to AMP HTML Specification an AMP document must:

  • start with the doctype < !doctype html>
  • contain a top-level <html &#x26a1;> tag (</html><html amp> is accepted as well)
  • contain <head> and <body> tags (They are optional in HTML)
  • contain a <link rel="canonical" href="$SOME_URL"/> tag inside their head that points to the regular HTML version of the AMP HTML document or to itself if no such HTML version exists
  • contain a <meta charset="utf-8"/> tag as the first child of their head tag
  • contain a <meta name="viewport" content="width=device-width,minimum-scale=1"/> tag inside their head tag. It’s also recommended to include initial-scale=1
  • contain a <script async src="https://cdn.ampproject.org/v0.js"></script> tag inside their head tag
  • contain the AMP boilerplate code (head > style[amp-boilerplate] and noscript > style[amp-boilerplate]) in their head tag

If this was all that AMP required for compliance I’d be ok. I’ve always been a gung-ho fan of proper page structure and including the appropriate elements where they should be. Death to tagsoup markup!

I love the idea of having metadata inlined in the document. This makes it easier to enforce (it’s part of the AMP HTML spec) but it may make it harder to support in the long run as any change to the doc would require.

I may even forgive the extra attribute (other than class) in the HTML element.

But this is just the beginning.

Amp not only requires a certain structure from your HTML page but it changes some elements for AMP-specific ones and completely disallows others. I’m mostly OK with the tag it forbids as they are mostly legacy elements that have been kept on the specifications for compatibility (don’t break the web) reasons. I mean, seriously, frameset and frames? It’s 2018 people… we have better ways of doing that!

The table below (also from the AMP HTML Specification) explains the limitations and changes that AMP makes to standard HTML elements. Some of these changes help explain the structure of the example page seen earlier.

Tag Status in AMP HTML
script Prohibited unless the type is application/ld+json. (Other non-executable values may be added as needed.) Exception is the mandatory script tag to load the AMP runtime and the script tags to load extended components.
noscript Allowed. Can be used anywhere in the document. If specified, the content inside the <noscript> element displays if JavaScript is disabled by the user.
base Prohibited.
img Replaced with amp-img.
Please note: <img> is a Void Element according to HTML5, so it does not have an end tag. However, <amp-img> does have an end tag </amp-img>.
video Replaced with amp-video.
audio Replaced with amp-audio.
iframe Replaced with amp-iframe.
frame Prohibited.
frameset Prohibited.
object Prohibited.
param Prohibited.
applet Prohibited.
embed Prohibited.
form Allowed. Require including amp-form extension.
input elements Mostly allowed with exception of some input types, namely, <input[type=image]>, <input[type=button]>, <input[type=password]>, <input[type=file]> are invalid. Related tags are also allowed: <fieldset>, <label>
button Allowed.
style Required style tag for amp-boilerplate. One additional style tag is allowed in head tag for the purpose of custom styling. This style tag must have the attribute amp-custom.
link rel values registered on microformats.org are allowed. If a rel value is missing from our whitelist, please submit an issue. stylesheet and other values like preconnect, prerender and prefetch that have side effects in the browser are disallowed. There is a special case for fetching stylesheets from whitelisted font providers.
meta The http-equiv attribute may be used for specific allowable values; see the AMP validator specification for details.
a The href attribute value must not begin with javascript:. If set, the target attribute value must be _blank. Otherwise allowed.
svg Most SVG elements are allowed.

But it’s the changes that they make to standard HTML elements that worry me. Do we really need an image custom element? video? audio?

The docs say it’s ok to use link rel=manfiest but there are no references in the specification that `link rel=”manifest” is allowed.

I don’t think this solves the underlying problems of the web. It doesn’t change the fact that loading speed and payload size numbers are getting slower and bigger, not faster and smaller for the mobile web.

See the HTTP Archive comparison data between January 1, 2017, and March 15, 2018 for an idea of what I’m talking about.

Granted, the HTTPArchive crawl may not catch AMP pages in its mobile results and that the pages in the crawl may not have a link to the AMP version of the page so the numbers may not reflect the actual improvements (if any) that AMP has made on the web. That said, I expect the numbers to change, one way or another, in future HTTP Archive crawls as they incorporate Wappalyzer as a tool in future crawls.

But AMP doesn’t change the fact that we still have megabytes of images, even if they are compressed (if we get more space we’ll just load more of them) huge bundles of JavaScript that will take tens of seconds to load and unblock page rendering on the web.

Even though people, companies and doc sites have been promoting best practices for years there has been no real improvement on how we work on the web and AMP, by hiding all the implementation details of lazy loading,

There are many more solutions outside of AMP that will give you performant web content. I think the biggest issue is that people may not know or may not care about these new technologies and smaller libraries that will take care of some performance bottlenecks.

Generating AMP?

Handwriting AMP is different than writing HTML but how do we generate AMP when working with CMS other than WordPress? How much do we need to retool our systems based on the restrictions imposed by AMP?

Condé Nast has published information on how they generate the AMP content for each of their publications: Allure, Architectural Digest, Ars Technica, Backchannel, Bon Appétit, Brides, Condé Nast Traveler, Epicurious, Glamour, Golf Digest, GQ, Pitchfork, Self, Teen Vogue, The New Yorker, Vanity Fair, Vogue, W and Wired.

Their system is fairly intricate as the image below illustrates:

AMP Generation Process at Condé Nast. From The Why and How of Google AMP at Condé Nast

And the AMP generation process itself looks like it’s rather complex, running from Markdown to React that then converts the Markdown content into valid AMP HTML:

Condé Nast AMP Service Pipeline. From The Why and How of Google AMP at Condé Nast

So now we have to insert a way to identify when we want our CMS to serve AMP content and an AMP conversion process into our pipeline… but only for our AMP optimized content, right? I go back to the question I asked earlier about creating AMP only sites: Are they necessary?

Furthermore, I’m concerned about the amount of work needed to get AMP into a CMS. If you’re not using a commercially available platform, how do you build AMP in addition to the standard content? As we saw in Converting your content? the restrictions of an AMP page are different than those of normal HTML (even if some of the restrictions are sensible and make sense). Sure we have WordPress and several other CMS platforms from AMP’s Supported Platforms, Vendors, and Partners page:

Another thing that comes to mind when it comes to generating AMP programmatically is how much of the CSS we use in our normal site can we use with AMP?

The AMP specification requires developers to use no more than 50kb of CSS and, on its face, it’s a sensible requirement. That is until we realize that CSS animations and any extra prefixing we need to accommodate browser idiosyncrasies would also fall under that limitation and we need to decide early on how will we make the CSS fit into the 50KB requirements or how much to take out to make the CSS fit the size restriction and still keep branding folks happy.

This is not to say we should dump hundred and hundreds of lines of useless CSS into our pages but we should be the ones who decide how much CSS we use and how the CSS we use interacts with the existing content of the page. If we need to create one stylesheet for all our pages we can run them through UNCSS or similar tool to only serve the CSS the page actually uses and we also have the choice to inline it on the page.

Same thing with JavaScript.

How many AMP tags are too many?

There is a growing number of purpose-specific custom AMP elements. I can understand the need for amp-img and amp-video but I become more skeptic when I see amp-vimeo, amp-youtube,amp-brid-player or amp-kaltura-player. There may be specialized features that require the special elements but I don’t think that’s the case for all of them.

Below is the list of custom media elements that are part of the AMP ecosystem (amp-img is also part of the list but I removed it because it’s built into the AMP library):

  • amp-3d-gltf
  • amp-3q-player
  • amp-anim
  • amp-apester-media
  • amp-audio
  • amp-bodymovin-animation
  • amp-brid-player
  • amp-brightcove
  • amp-dailymotion
  • amp-google-vrview-image
  • amp-hulu
  • amp-ima-video
  • amp-imgur
  • amp-izlesene
  • amp-jwplayer
  • amp-kaltura-player
  • amp-nexxtv-player
  • amp-o2-player
  • amp-ooyala-player
  • amp-playbuzz
  • amp-reach-player
  • amp-soundcloud
  • amp-springboard-player
  • amp-video
  • amp-vimeo
  • amp-wistia-player
  • amp-youtube

It’s interesting that there is an AMP-specific Vimeo and element when the project itself describes how you can use amp-iframe to create the same experience:

<amp-iframe width="500"
  title="Netflix House of Cards branding: The Stack"
  height="281"
  layout="responsive"
  sandbox="allow-scripts allow-same-origin allow-popups"
  allowfullscreen
  frameborder="0"
  src="https://player.vimeo.com/video/140261016">
</amp-iframe>

Same for the Youtube element (although it doesn’t appear in the amp-iframe docs. All that we do is change the tag name to amp-iframe and add layout="responsive" and sandbox= allow-scripts allow-same-origin allow-popups.

<amp-iframe
  layout="responsive"
  sandbox="allow-scripts allow-same-origin allow-popups"
  width="560"
  height="315"
  src="https://www.youtube.com/embed/3vcGsrMdiUA?rel=0"
  frameborder="0"
  allow="autoplay; encrypted-media"
  allowfullscreen
  playsinline></amp-iframe>

There is nothing you can do in the amp-youtube that you can’t do using an amp-iframe element. So why the difference? Are the optimizations needed for a given player enough reason to create another custom element, another script to load and another item to prerender and, pontentially, slow the user experience for people in slower connections?

Specific Use Cases

My specific cases of what AMP doesn’t allow are Codepen embeds and Prism.js code syntax highlighting.

The typical expected Codepen emebd looks like this:

<p data-height="438" data-theme-id="dark" data-slug-hash="MGWazx" data-default-tab="html,result" data-user="caraya" data-embed-version="2" data-pen-title="Card Grid Component for Themeable Design System" class="codepen">See the Pen <a href="https://codepen.io/caraya/pen/MGWazx/">Card Grid Component for Themeable Design System</a> by Carlos Araya (<a href="https://codepen.io/caraya">@caraya</a>) on <a href="https://codepen.io">CodePen</a>.</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

As far as I understand AMP, this would not work in an AMP page. AMP will not let you use script tags in a page, and even if AMP allows data-* attributes on elements they are useless because there’s nothing to process them with.

Same thing with Prism. It works by adding a script and a style sheet to the page and then expects properly coded blocks of preformatted text for the content to be highlighted. To validate AMP content I am not allowed to have additional script tags on the head or before the end of my document. In theory, I should be able to merge Prism’s CSS with the AMP required CSS and my site’s CSS but would that work in 50KB of CSS that we’re allowed to add?

And yes, I know that Codepen also provides iframes for embedding pens but it’s not the recommended form to do so.

The problem with Prism is more involved and something that I’m still trying to figure out. It requires workers but I’m still trying to figure out how to make them talk to each other.

CORS for my own site?

One of the biggest issues is hosting the content in a Google-based and supported CDN. This causes the large, ugly URLs (like https://www-gq-com.cdn.ampproject.org/c/s/www.gq.com/story/gq-eye-exclusive-fatherhood-style-series---todd-snyder or https://www-gq-com.cdn.ampproject.org/c/s/www.gq.com/) but also has an interesting side effect that makes CORS necessary for your own content.

When you go to a site, for example: https://www.gq.com/story/gq-eye-exclusive-fatherhood-style-series---todd-snyder in a mobile device it’ll redirect you to the version hosted in the AMP CDN which turns into https://www-gq-com.cdn.ampproject.org/c/s/www.gq.com/story/gq-eye-exclusive-fatherhood-style-series---todd-snyder and caches it to guarantee preloads and seemingly instant first load experiences.

But what happens if you need to load data for your application? Say you want to load product data for your shopping cart, or a tour date list for your favorite band? Those are stored on your server, not the cache and for them to work properly you need to set CORS headers on them so they will work for origins other than yours.

On a side note, if you hit the AMP cache URL from a desktop browser it will redirect you to the desktop, however, if you hit an AMP link using an activation script like https://www.gq.com/story/gq-eye-exclusive-fatherhood-style-series---todd-snyder/amp it will take you to the AMP version, regardless of platform or form factor. I find this even more interesting, why restrict some ways to access the content from some form factors and not others? Am I missing the point of the URLs?

Performance

People have been working with webperformance for years. The early work Ilia Grigorik and Addy Osmani did and continue to do at Google, tools like UNCSS, the fact that we can precache and preload content (but not provide automatic preloading like Google does with AMP search results), the fact that we have PWA and its component technologiestechnologiies available within and outside PWAs really makes me wonder how lazy we’ve become as developers.

I’ll take three examples of things we can do now to make experiences outside of an AMP environment: Image Optimization, Resource PrioritizationPioritization, and Service Workers.

We know that images make up for a large fraction of what gets pushed into a web page, we don’t evangelize tools like Imagemin and workflows that use these tools to generate smaller images.

Does amp-img really reduce the bloat problem? If you make the images smaller that means people will put even more images in a page because the size of each individual image is smaller so we can use more of them, we don’t need to worry about the individual image, AMP will take care of that.

It is not hard to create a good resource prioritization strategy using a combination of H2 Push, link preload, prefetch, prerender and dns-prefetch to load resources the way we want them to. If we compare this with lazy loading content so they will only load when needed, we have a much better web experience.

There is no current 100% foolproof way to get H/2 push to work consistently in all browsers as discussed in Jake Archibald’s HTTP2 is tougher than I thought. As Jake points out in the article’s conclusion:

There are some pretty gnarly bugs around HTTP/2 push right now, but once those are fixed I think it becomes ideal for the kinds of assets we currently inline, especially render-critical CSS. Once cache digests land, we’ll hopefully get the benefit of inlining, but also the benefit of caching.

Getting this right will depend on smarter servers which allow us to correctly prioritize the streaming of content. Eg, I want to be able to stream my critical CSS in parallel with the head of the page, but then give full priority to the CSS, as it’s a waste spending bandwidth on body content that the user can’t yet render.

We’re not there yet, but we can do something to make the whole load faster with a little help from the server. We create server-side user agent sniffing strings based on case-insensitive matching with BrowserMatchNoCase directive, part of the mod_setenvif module.

# Windows 10-based PC using Edge browser
BrowserMatchNoCase edge browser=Edge

# Chrome OS-based laptop using Chrome browser (Chromebook)
BrowserMatchNoCase CrOS browser=ChromeOS

# Mac OS X-based computer using a Safari browser
BrowserMatchNoCase safari browser=Safari

# Windows 7-based PC using a Chrome browser
BrowserMatchNoCase chrome browser=chrome

# Linux-based PC using a Firefox browser
BrowserMatchNoCase firefox browser=firefox

We can then test if the content has been pushed to the client using cookies as described in Web Performance Improvement and serve resources based on the client browser and its current limitations.

We need to be aware that the cookie we set from the server as we push the resources is not completely reliable after the first time we push resources; that may cause us to miss resources or to load them more than once.

Service workers can also help improve the performance of your web content after the initial visit. We can use it to keep a persistent cache of content for our site or application that can be populated and expired as needed; we can also modify the requests as needed and ameliorate the problems from h/2 push alone.

Libraries like Workbox can help us make this process easier.

A more radical solution is to server-side render your application. This will take care of perceived performance issues at the cost of potentially longer initial load times. Since the server has to process all the Javascript to render the content on the page. Eric Bidelman presents a way to use Puppeteer to SSR an Express Node application. Other frameworks like React and Vue have their own systems to server-side render applications. It is up to you to decide if the extra work and the extra time it’ll take for your application to render on the server is worth it.

One of the ways AMP makes content load faster is by pre-rendering content from the Google Search Result Page. Note how the pre-render only works for valid AMP pages. Malte Ubl, the tech lead for AMP, wrote a Medium post outlining why they chose to do so. While it’s nice that they can be so aggressive in their prerendering of their custom elements it still raises some questions about implementation and presents some interesting questions when talking about prerendering resources outside the AMP environment.

If you have more than one resource that the amp-img element is supposed to load, will it prerender all of them? For example in the following element, how will the AMP runtime know which image to prerender for browsers that may or may not support WebP?

<amp-img alt="Mountains"
  width="550"
  height="368"
  src="images/mountains.webp">
  <amp-img alt="Mountains"
    fallback
    width="550"
    height="368"
    src="images/mountains.jpg"></amp-img>
</amp-img>

Similar but even more worrisome is the case of responsive images. The example below offers ten different options for this one single image (not counting 2x and 3x images for retina displays). Will the browser download all of them if the image is above the fold? Are images subject to the restriction of placement from the top of the page?

<amp-img src="/img/amp.jpg"
  srcset="  /img/amp.jpg 1080w,
            /img/amp-900.jpg 900w,
            /img/amp-800.jpg 800w,
            /img/amp-700.jpg 700w,
            /img/amp-600.jpg 600w,
            /img/amp-500.jpg 500w,
            /img/amp-400.jpg 400w,
            /img/amp-300.jpg 300w,
            /img/amp-200.jpg 200w,
            /img/amp-100.jpg 100w"
  width="1080"
  height="610"
  layout="responsive"
  alt="AMP"></amp-img>

We do have the options of prerendering content outside of the AMP ecosystem but the question becomes: How much? Is it worth taking what Steve Souders calls “the nuclear option” of preloading your content? How do you pick what content to prerender? Do we prerender all the links in our page? All the internal links? External?

Google has the resources to proactively prerender the AMP pages it serves from the AMP cache and that’s the only way we can take full advantage of AMP, using their technology and their delivery mechanism. This doesn’t mean that AMP’s prerender mechanism is the best way to go as it trades speed for bandwidth.

I recently came across Tim Kadlec’s How Fast Is Amp Really? performance analysis of AMP content served in different ways:

  1. How well does AMP perform in the context of Google search?
  2. How well does the AMP library perform when used as a standalone framework?
  3. How well does AMP perform when the library is served using the AMP cache?
  4. How well does AMP perform compared to the canonical article?

His findings mirror my opinion about AMP not doing anything that we can’t do in the open web. To me, the most, important section of Tim’s article is when he talks about page weight reduction:

The 90th percentile weight for the canonical version is 5,229kb. The 90th percentile weight for AMP documents served from the same origin is 1,553kb— a savings of around 70% in page weight. The 90th percentile request count for the canonical version is 647, for AMP documents it’s 151. That’s a reduction of nearly 77%.

AMP’s restrictions mean less stuff. It’s a concession publishers are willing to make in exchange for the enhanced distribution Google provides, but that they hesitate to make for their canonical versions.

If we’re grading AMP on the goal of making the web faster, the evidence isn’t particularly compelling. Every single one of these publishers has an AMP version of these articles in addition to a non-AMP version.

From: How Fast Is Amp Really?

AMP restricts what you can do with JavaScript and what you can do with custom AMP elements restricts what you can do on your pages and, regardless of the reasons why you put the restrictions in place, it shouldn’t be up to a library or framework to dictate how much stuff you put in a page but it’s up to the designers and developers to slim down their content… easier said than done, I know, but AMP hasn’t sold me as the solution or even a good solution to this problem and neither do publishers, otherwise AMP would be the only version of all published web content.

The other interesting aspect of this conversation is what happens to search engines like Bing or other platforms that don’t support canonical links?

The final, technical, question I have is how well AMP works in low-end and feature phones or for people using Proxy browsers such as Opera Mini, UC browser or Chrome for Android in their most aggressive data saving modes? How do publishers in those markets address the needs of their users while still remaining compliant with AMP requirements?