UNPKG

assetgraph

Version:

An auto discovery dependency graph based optimization framework for web pages and applications

199 lines (164 loc) 5.92 kB
/* * This transform works on an assumption about browser loading and rendering behavior of CSS: * When a browser loads CSS in <head> it render blocks untill all CSS in <head> has loaded. * * This might be slow. * * To appear faster, browsers will render what they have in buffer already if they meet a style <link> in <body> * before they dive into the render blocking load. * This lets you potentially inline critical css in <head> and put the bulk of your CSS "below the fold". * The effect will be a quick render with critical styles applied, just before the render blocking starts. * * We can leverage this behavior by letting the developer declaratively define their critical CSS if we * treat <body> <link> ad "the fold", assuming that everything above it is critical and should have styling applied * for the render flush that happens just before the <link> starts loading. * * Assetgraph can figure out what CSS to extract from the bundle and inline in <head> before the render blocking load starts. * This way the developer will only have to place the <link> at the relevant place in the DOM and not think any more about * builds and optimization. */ const postcss = require('postcss'); const stripDynamicPseudoClasses = require('../util/stripDynamicPseudoClasses'); function getDomNodesAboveFold(fold) { let currentNode = fold; const criticalNodes = []; while (currentNode) { if (currentNode.nodeType === 1) { criticalNodes.push(currentNode); } if (currentNode.tagName === 'BODY') { criticalNodes.push(currentNode.parentNode); // Lets get the <html> node in there as well break; } if (currentNode.previousSibling) { // Collect all child nodes of previous siblings if (currentNode.previousSibling.nodeType === 1) { criticalNodes.push( ...currentNode.previousSibling.querySelectorAll('*') ); } currentNode = currentNode.previousSibling; } else { currentNode = currentNode.parentNode; } } return criticalNodes; } function getClonedAtRuleScaffold(rule, root, clonedAtRuleMap) { const cacheHit = clonedAtRuleMap.find(mapping => { if (mapping.original === rule) { return mapping.clone; } }); if (cacheHit) { return cacheHit.clone; } const clonedRule = rule.clone(); clonedRule.removeAll(); clonedAtRuleMap.push({ original: rule, clone: clonedRule }); if (rule.parent.type === 'atrule') { const parentRule = getClonedAtRuleScaffold( rule.parent, root, clonedAtRuleMap ); clonedRule.parent = parentRule; parentRule.nodes.push(clonedRule); } else { clonedRule.parent = root; root.nodes.push(clonedRule); } return clonedRule; } module.exports = () => { return function inlineCriticalCss(assetGraph) { const clonedAtRuleMap = []; // For each html page with <link> tags in <body> and not in <head> const pages = assetGraph .findAssets({ type: 'Html', isLoaded: true, isInline: false }) .filter(page => { const relations = assetGraph.findRelations({ type: 'HtmlStyle', from: page, to: { isInline: false } }); if (relations.length === 0) { return false; } return relations.every(relation => relation.node.matches('body link')); }); for (const page of pages) { // Find the first external HtmlStyle relation inside <body> const cssRelations = assetGraph.findRelations({ type: 'HtmlStyle', from: page, to: { isInline: false } }); const fold = cssRelations[0].node; // Traverse back up the DOM to find all elements to check critical CSS for const criticalNodes = getDomNodesAboveFold(fold); // Find all CSS included on the page const stylesheets = assetGraph .collectAssetsPreOrder(page, { to: { type: 'Css' } }) .filter(asset => asset.type === 'Css'); const criticalParseTree = postcss.parse(''); for (const stylesheet of stylesheets) { // For each critical DOM-node, check if each style rule applies stylesheet.eachRuleInParseTree(cssRule => { if (!cssRule.selector) { return; } const nonPseudoSelector = stripDynamicPseudoClasses(cssRule.selector); const criticalMatch = criticalNodes.some(node => { let match; try { match = node.matches(nonPseudoSelector); } catch (err) { // jsdom doesn't understand this selector } return match; }); // Collect all matching style rules that match critical DOM-nodes if (criticalMatch) { const clonedRule = cssRule.clone(); if (cssRule.parent.type === 'atrule') { const parentRule = getClonedAtRuleScaffold( cssRule.parent, criticalParseTree, clonedAtRuleMap ); clonedRule.parent = parentRule; parentRule.nodes.push(clonedRule); } else { clonedRule.parent = criticalParseTree; criticalParseTree.nodes.push(clonedRule); } } }); } // Inline critical stylesheet in <head> if (criticalParseTree.nodes.length > 0) { if (page.parseTree.querySelector('html')) { page.addRelation( { type: 'HtmlStyle', to: { type: 'Css', parseTree: criticalParseTree } }, 'lastInHead' ); } else { assetGraph.warn( new Error( `Cannot inline the critical CSS in ${page.urlOrDescription}, the page does not have an <html> element` ) ); } } } }; };