Skip to content

Add runtime CSS minification, !important replacement, and tree shaking#1048

Merged
westonruter merged 32 commits intodevelopfrom
add/css-tree-shaking
Apr 6, 2018
Merged

Add runtime CSS minification, !important replacement, and tree shaking#1048
westonruter merged 32 commits intodevelopfrom
add/css-tree-shaking

Conversation

@westonruter
Copy link
Copy Markdown
Member

@westonruter westonruter commented Mar 30, 2018

  • Add support for Server Timing to reveal the time required for the plugin to render a response. Move header-sending logic to AMP_Response_Headers class. See Incorporate Server Timing API #990.
  • Minify CSS to remove whitespace and comments. See CSS in Reader mode is not minified #688.
  • Sanitize CSS by means of a CSS parser instead of relying on regular expressions.
  • Remove illegal CSS properties (-moz-binding and behavior).
  • Reduce length of CSS class names generated for style attributes.
  • If amp-keyframes appears anywhere but end of body, just move it to the end of the body.
  • Implement the replace-important approach to eliminating !important qualifiers.
  • Delete illegal CSS @-rules.
  • Make sure css_spec gets parsed and included in AMP_Allowed_Tags_Generated.
  • Limit @keyframes to opacity, transform and -vendorPrefix-transform.
  • Make sure legacy post template still renders properly with stylesheet changes.
  • Delete CSS rules that don't apply to the current page based on the class names used, if the total CSS size would otherwise be greater than 50KB. (CSS tree shaking.)
  • Delete selectors that reference class names that don't exist.
  • Handle case of false negatives when a selector contains :not(.classname). Also strip strings.
  • Figure out how to gracefully deal with Genericons and Dashicons.
  • Use expiring transient instead of indefinite cache when external cache is not available.
  • Improve handling of specificity, including giving higher selector specificity to rules from inline styles. Reduce length of fake ID.
  • Address relative URLs, such as for background images.

Fixes #990.
Fixes #930.
Fixes #688.
Closes #610.

Ideas for later:

  • Look at other tree-shaking methods beyond class names.
  • Add tree-shaking based on IDs.

@westonruter westonruter added this to the v1.0 milestone Mar 30, 2018
@westonruter westonruter self-assigned this Mar 30, 2018
* Compress CSS output with compact format, removing whitespace and comments.
* Reduce length of generated class names for inline styles.
* Unify logic for handling inline style attributes with handling stylesheets in style/link.
…c selectors (following replace-important package)

Eliminate illegal_css_important_qualifier validation error now that transformed
@westonruter westonruter force-pushed the add/css-tree-shaking branch from 73ad61a to fd04b20 Compare March 31, 2018 02:04
* Handle CSS parse errors.
* Let spec inform which @-rules are allowed.
* Improve handling of vendor-prefixed properties in whitelist/blacklists.
* Inform when !important qualifier is removed from rules with selectors (that aren't keyframes).
$selectors = array();
if ( $should_tree_shake ) {
foreach ( $selectors_parsed as $selector => $class_names ) {
if ( count( $class_names ) === count( array_intersect( $class_names, $this->used_class_names ) ) ) {
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better to use array_diff here instead maybe.

Copy link
Copy Markdown
Contributor

@kienstra kienstra left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great Work

Hi @westonruter,
This PR is a big step forward for the plugin. It opens a lot of possibilities, including Gutenberg support.

As you know, the Gutenberg build/style.css file was too big for the 50 KB limit before.

It looks like there's still some discussion here. I added a few minor points, but I'm happy to approve this when it's ready.

*
* @param array $query_vars Query vars.
* @return array Query vars.
*/
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice improvements here with the DocBlocks and @since tags.

'error_message' => 'CSS !important',
'regex' => '!important',
),
'css_spec' => array(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This corresponds well to the css_spec in the amphtml repo's validator-main.protoascii.

/**
* Class AMP_Response_Headers
*/
class AMP_Response_Headers {
Copy link
Copy Markdown
Contributor

@kienstra kienstra Apr 2, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's nice you you put this in a separate class 👍

@@ -0,0 +1,116 @@
<?php
/**
* Tests for Theme Support.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This DocBlock should probably be Tests for AMP_Response_Headers, and the same below.

}
}

return $stylesheet;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it help to call the validation_error_callback here if $stylesheet is empty and it's from a <link>? This would account for when a stylesheet has no rules that pertain to DOM classes. Like if a plugin enqueues a stylesheet for its shortcode on every request.

if ( empty( $stylesheet ) && ( 'link' === $node->nodeName ) && ! empty( $this->args['validation_error_callback'] ) ) {
        // Call validation callback

We'd probably have to deprecate amp-default.css to do this.

@amedina amedina requested a review from pbakaus April 3, 2018 19:05
@amedina
Copy link
Copy Markdown
Member

amedina commented Apr 3, 2018

@westonruter
Copy link
Copy Markdown
Member Author

@amedina yes, I referred to replace-important for the implementation here. I think it is improved on here in a couple ways:

  • It uses _ instead of FK_ID as the dummy ID for the :not() selector. This is important because the :not() has to repeated to achieve the required specificity.
  • It also accounts for adding specificity to rules like html { color:red !important; } in that it amends :not(#_) to the html root selector, as opposed to adding an additional ancestor selector (which shouldn't be valid). So here this becomes html:not(#_) { color:red; } instead of :root:not(#_) html { color:red; }.

@westonruter
Copy link
Copy Markdown
Member Author

the point you make in the doc about amp-live-list is a key one: content could be added to the page later that depends on a rule that is filtered

Just occurred to me that we could help guard against this from happening by adding logic to skip removing selectors that reference amp-live-list or amp-list elements. Also, we could skip selectors that reference div[submit-success] and div[submit-error].

Nevertheless, there would be no way to statically account for class names assigned via amp-bind, so that's a danger, unless we adopt some some naming convention for amp-bind-supplied class names used in selectors.

@pbakaus Any other key dynamic content cases to have in mind?

@westonruter westonruter force-pushed the add/css-tree-shaking branch from b278bc2 to 9a6cec0 Compare April 5, 2018 06:08
@westonruter westonruter force-pushed the add/css-tree-shaking branch from 9a6cec0 to 313f0b9 Compare April 5, 2018 06:29
@pbakaus
Copy link
Copy Markdown

pbakaus commented Apr 5, 2018

@westonruter sounds sensible! Thats the only ones I have in mind right now.

@camelburrito
Copy link
Copy Markdown

@westonruter usually the ones associated with amp-bind can be searchable in the dom (string search), even that would be a little risky because there could be string concat etc happening.
Naming conventions seems to be a sane idea. I guess these could be optional flag that we can enable (like list of prefixes that should not be deleted?)

And on the replace-important improvements feel free to edit the code here
https://github.com/ampproject/ampstart/tree/master/tools/replace-important
I can help you push it live. If that would help you keep it more modular.

@westonruter
Copy link
Copy Markdown
Member Author

I guess these could be optional flag that we can enable (like list of prefixes that should not be deleted?)

@camelburrito I added initial support for this via the dynamic_element_selectors argument in
bede9de. By default it only includes the known dynamic content ancestors, but a given theme could amend it with additional dynamic selectors to exclude from removal.

@kienstra This is ready for review to merge so we can get this testing. I want to follow up later with some more improvements including the added ability to knowingly bypass removal of elements, attributes, and styles that are invalid but which a given site cannot afford to be removed. I'm thinking we'd want CSS over the 50KB limit to default to not be removed, even if that means it is invalid AMP. In terms of implementation here, I think this can be implemented by allowing the validation_error_callback to return false as a way to prevent removal from happening. That would allow a theme/plugin to decide on a case-by-case basis which things should get removed. This is in regards to #1048 (review) and #1048 (comment)

@westonruter westonruter assigned kienstra and unassigned westonruter Apr 6, 2018
Copy link
Copy Markdown
Contributor

@kienstra kienstra left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved

Hi @westonruter,
This PR looks good, and it's approved. There's a small point about test_body_style_attribute_sanitizer(), but it's not a blocker.

Also:

allowing the validation_error_callback to return false as a way to prevent removal from happening

That sounds good, finalize_stylesheet_set() could check the return value of the validation_error_callback before removing the stylesheet.

* @type callable $validation_error_callback Function to call when a validation error is encountered.
* }
*/
protected $args;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice DocBlock for this.

array(),
),
'styles_with_dynamic_elements' => array(
implode( '', array(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great idea to use implode() here for the long document.

* @param array $expected_errors Expected error codes.
*/
public function test_body_style_attribute_sanitizer( $source, $expected_content, $expected_stylesheets ) {
public function test_body_style_attribute_sanitizer( $source, $expected_content, $expected_stylesheets, $expected_errors = array() ) {
Copy link
Copy Markdown
Contributor

@kienstra kienstra Apr 6, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe the 3rd $expected_errors parameter isn't necessary, given that the dataProvider get_link_and_style_test_data always has a 3rd array item: array(). But this isn't a blocker to approving.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, you're right. But I suppose it aligns with the other tests and opens the door to testing for validation errors in the future.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Incorporate Server Timing API Extend style sanitizer to include CSS parser CSS in Reader mode is not minified

8 participants