Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 32 commits into from
Apr 6, 2018

Conversation

westonruter
Copy link
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
* 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
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.

@westonruter
Copy link
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
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?

@pbakaus
Copy link

pbakaus commented Apr 5, 2018

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

@camelburrito
Copy link

@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
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
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
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
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.

*/
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
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
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
7 participants