Skip to content

Commit

Permalink
feature: configuration for css inlining behavior (#6659)
Browse files Browse the repository at this point in the history
* feature(inline stylesheets): implement as experimental

* test: rename css-inline -> css-import-as-inline

* test(content collections): add de-duplication of css

* test: add new suite for inlineStylesheets configuration

* fix(inline stylesheets): did not act on propagated styles

* hack(inline stylesheets testing): duplicate fixtures

Content collections reuses build data across multiple fixture.builds, even though a configuration change may have changed it.
Duplicating fixtures avoids usage of the stale cache.
https://cdn.discordapp.com/attachments/1039830843440504872/1097795182340092024/Screenshot_87_colored.png

* refactor(css plugin): reduce nesting

* optimization(css rendering): merge <style> tags

Chrome, but not Safari or Firefox, is slower to match rules when they are split across multiple files or style tags.
https://nolanlawson.com/2022/06/22/style-scoping-versus-shadow-dom-which-is-fastest/

Having the abiility to inline stylesheets opens us up to this optimization.
Ideally, it would extend to propagated styles, but that ended up being a rabbit hole.

* typedocs(inlineStylesheets config): ensure consistency

Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>

* chore(build internals): update comment

* correct minor mistake in test

* test(inline stylesheets): unique package names for duplicate fixtures

* refactor(css build plugin): maps -> records

* refactor(css build plugin): remove use of spread operator

---------

Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
  • Loading branch information
lilnasy and sarah11918 authored May 3, 2023
1 parent 8d75340 commit 80e3d4d
Show file tree
Hide file tree
Showing 49 changed files with 1,355 additions and 310 deletions.
5 changes: 5 additions & 0 deletions .changeset/friendly-fishes-sing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': minor
---

Implement Inline Stylesheets RFC as experimental
20 changes: 20 additions & 0 deletions packages/astro/src/@types/astro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1036,6 +1036,26 @@ export interface AstroUserConfig {
*/
assets?: boolean;

/**
* @docs
* @name experimental.inlineStylesheets
* @type {('always' | 'auto' | 'never')}
* @default `never`
* @description
* Control whether styles are sent to the browser in a separate css file or inlined into <style> tags. Choose from the following options:
* - `'always'` - all styles are inlined into <style> tags
* - `'auto'` - only stylesheets smaller than `ViteConfig.build.assetsInlineLimit` (default: 4kb) are inlined. Otherwise, styles are sent in external stylesheets.
* - `'never'` - all styles are sent in external stylesheets
*
* ```js
* {
* experimental: {
* inlineStylesheets: `auto`,
* },
* }
*/
inlineStylesheets?: 'always' | 'auto' | 'never';

/**
* @docs
* @name experimental.middleware
Expand Down
13 changes: 10 additions & 3 deletions packages/astro/src/content/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
createHeadAndContent,
renderComponent,
renderScriptElement,
renderStyleElement,
renderTemplate,
renderUniqueStylesheet,
unescapeHTML,
Expand Down Expand Up @@ -152,13 +151,21 @@ async function render({
links = '',
scripts = '';
if (Array.isArray(collectedStyles)) {
styles = collectedStyles.map((style: any) => renderStyleElement(style)).join('');
styles = collectedStyles
.map((style: any) => {
return renderUniqueStylesheet(result, {
type: 'inline',
content: style,
});
})
.join('');
}
if (Array.isArray(collectedLinks)) {
links = collectedLinks
.map((link: any) => {
return renderUniqueStylesheet(result, {
href: prependForwardSlash(link),
type: 'external',
src: prependForwardSlash(link),
});
})
.join('');
Expand Down
16 changes: 12 additions & 4 deletions packages/astro/src/content/vite-plugin-content-assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,8 @@ export function astroConfigBuildPlugin(
chunk.type === 'chunk' &&
(chunk.code.includes(LINKS_PLACEHOLDER) || chunk.code.includes(SCRIPTS_PLACEHOLDER))
) {
let entryCSS = new Set<string>();
let entryStyles = new Set<string>();
let entryLinks = new Set<string>();
let entryScripts = new Set<string>();

for (const id of Object.keys(chunk.modules)) {
Expand All @@ -137,7 +138,8 @@ export function astroConfigBuildPlugin(
const _entryScripts = pageData.propagatedScripts?.get(id);
if (_entryCss) {
for (const value of _entryCss) {
entryCSS.add(value);
if (value.type === 'inline') entryStyles.add(value.content);
if (value.type === 'external') entryLinks.add(value.src);
}
}
if (_entryScripts) {
Expand All @@ -150,10 +152,16 @@ export function astroConfigBuildPlugin(
}

let newCode = chunk.code;
if (entryCSS.size) {
if (entryStyles.size) {
newCode = newCode.replace(
JSON.stringify(STYLES_PLACEHOLDER),
JSON.stringify(Array.from(entryStyles))
);
}
if (entryLinks.size) {
newCode = newCode.replace(
JSON.stringify(LINKS_PLACEHOLDER),
JSON.stringify(Array.from(entryCSS).map(prependBase))
JSON.stringify(Array.from(entryLinks).map(prependBase))
);
}
if (entryScripts.size) {
Expand Down
7 changes: 5 additions & 2 deletions packages/astro/src/core/app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
import { RouteCache } from '../render/route-cache.js';
import {
createAssetLink,
createLinkStylesheetElementSet,
createStylesheetElementSet,
createModuleScriptElement,
} from '../render/ssr-element.js';
import { matchRoute } from '../routing/match.js';
Expand Down Expand Up @@ -180,7 +180,9 @@ export class App {
const url = new URL(request.url);
const pathname = '/' + this.removeBase(url.pathname);
const info = this.#routeDataToRouteInfo.get(routeData!)!;
const links = createLinkStylesheetElementSet(info.links);
// may be used in the future for handling rel=modulepreload, rel=icon, rel=manifest etc.
const links = new Set<never>();
const styles = createStylesheetElementSet(info.styles);

let scripts = new Set<SSRElement>();
for (const script of info.scripts) {
Expand All @@ -203,6 +205,7 @@ export class App {
pathname,
componentMetadata: this.#manifest.componentMetadata,
scripts,
styles,
links,
route: routeData,
status,
Expand Down
5 changes: 5 additions & 0 deletions packages/astro/src/core/app/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ import type {

export type ComponentPath = string;

export type StylesheetAsset =
| { type: 'inline'; content: string }
| { type: 'external'; src: string };

export interface RouteInfo {
routeData: RouteData;
file: string;
Expand All @@ -21,6 +25,7 @@ export interface RouteInfo {
// Hoisted
| { type: 'inline' | 'external'; value: string }
)[];
styles: StylesheetAsset[];
}

export type SerializedRouteInfo = Omit<RouteInfo, 'routeData'> & {
Expand Down
47 changes: 38 additions & 9 deletions packages/astro/src/core/build/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,25 @@ import { createEnvironment, createRenderContext, renderPage } from '../render/in
import { callGetStaticPaths } from '../render/route-cache.js';
import {
createAssetLink,
createLinkStylesheetElementSet,
createStylesheetElementSet,
createModuleScriptsSet,
} from '../render/ssr-element.js';
import { createRequest } from '../request.js';
import { matchRoute } from '../routing/match.js';
import { getOutputFilename } from '../util.js';
import { getOutDirWithinCwd, getOutFile, getOutFolder } from './common.js';
import { eachPageData, getPageDataByComponent, sortedCSS } from './internal.js';
import type { PageBuildData, SingleFileBuiltModule, StaticBuildOptions } from './types';
import {
eachPageData,
getPageDataByComponent,
cssOrder,
mergeInlineCss,
} from './internal.js';
import type {
PageBuildData,
SingleFileBuiltModule,
StaticBuildOptions,
StylesheetAsset,
} from './types';
import { getTimeStat } from './util.js';

function shouldSkipDraft(pageModule: ComponentInstance, settings: AstroSettings): boolean {
Expand Down Expand Up @@ -161,8 +171,14 @@ async function generatePage(
const renderers = ssrEntry.renderers;

const pageInfo = getPageDataByComponent(internals, pageData.route.component);
const linkIds: string[] = sortedCSS(pageData);

// may be used in the future for handling rel=modulepreload, rel=icon, rel=manifest etc.
const linkIds: [] = [];
const scripts = pageInfo?.hoistedScript ?? null;
const styles = pageData.styles
.sort(cssOrder)
.map(({ sheet }) => sheet)
.reduce(mergeInlineCss, []);

const pageModule = ssrEntry.pageMap?.get(pageData.component);
const middleware = ssrEntry.middleware;
Expand All @@ -183,6 +199,7 @@ async function generatePage(
internals,
linkIds,
scripts,
styles,
mod: pageModule,
renderers,
};
Expand Down Expand Up @@ -273,6 +290,7 @@ interface GeneratePathOptions {
internals: BuildInternals;
linkIds: string[];
scripts: { type: 'inline' | 'external'; value: string } | null;
styles: StylesheetAsset[];
mod: ComponentInstance;
renderers: SSRLoadedRenderer[];
}
Expand Down Expand Up @@ -341,7 +359,15 @@ async function generatePath(
middleware?: AstroMiddlewareInstance<unknown>
) {
const { settings, logging, origin, routeCache } = opts;
const { mod, internals, linkIds, scripts: hoistedScripts, pageData, renderers } = gopts;
const {
mod,
internals,
linkIds,
scripts: hoistedScripts,
styles: _styles,
pageData,
renderers,
} = gopts;

// This adds the page name to the array so it can be shown as part of stats.
if (pageData.route.type === 'page') {
Expand All @@ -350,13 +376,15 @@ async function generatePath(

debug('build', `Generating: ${pathname}`);

const links = createLinkStylesheetElementSet(
linkIds,
// may be used in the future for handling rel=modulepreload, rel=icon, rel=manifest etc.
const links = new Set<never>();
const scripts = createModuleScriptsSet(
hoistedScripts ? [hoistedScripts] : [],
settings.config.base,
settings.config.build.assetsPrefix
);
const scripts = createModuleScriptsSet(
hoistedScripts ? [hoistedScripts] : [],
const styles = createStylesheetElementSet(
_styles,
settings.config.base,
settings.config.build.assetsPrefix
);
Expand Down Expand Up @@ -431,6 +459,7 @@ async function generatePath(
request: createRequest({ url, headers: new Headers(), logging, ssr }),
componentMetadata: internals.componentMetadata,
scripts,
styles,
links,
route: pageData.route,
env,
Expand Down
77 changes: 47 additions & 30 deletions packages/astro/src/core/build/internal.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Rollup } from 'vite';
import type { PageBuildData, ViteID } from './types';
import type { PageBuildData, StylesheetAsset, ViteID } from './types';

import type { SSRResult } from '../../@types/astro';
import type { PageOptions } from '../../vite-plugin-astro/types';
Expand Down Expand Up @@ -224,39 +224,56 @@ export function hasPrerenderedPages(internals: BuildInternals) {
return false;
}

interface OrderInfo {
depth: number;
order: number;
}

/**
* Sort a page's CSS by depth. A higher depth means that the CSS comes from shared subcomponents.
* A lower depth means it comes directly from the top-level page.
* The return of this function is an array of CSS paths, with shared CSS on top
* and page-level CSS on bottom.
* Can be used to sort stylesheets so that shared rules come first
* and page-specific rules come after.
*/
export function sortedCSS(pageData: PageBuildData) {
return Array.from(pageData.css)
.sort((a, b) => {
let depthA = a[1].depth,
depthB = b[1].depth,
orderA = a[1].order,
orderB = b[1].order;

if (orderA === -1 && orderB >= 0) {
return 1;
} else if (orderB === -1 && orderA >= 0) {
return -1;
} else if (orderA > orderB) {
return 1;
} else if (orderA < orderB) {
return -1;
} else {
if (depthA === -1) {
return -1;
} else if (depthB === -1) {
return 1;
} else {
return depthA > depthB ? -1 : 1;
}
}
})
.map(([id]) => id);
export function cssOrder(a: OrderInfo, b: OrderInfo) {
let depthA = a.depth,
depthB = b.depth,
orderA = a.order,
orderB = b.order;

if (orderA === -1 && orderB >= 0) {
return 1;
} else if (orderB === -1 && orderA >= 0) {
return -1;
} else if (orderA > orderB) {
return 1;
} else if (orderA < orderB) {
return -1;
} else {
if (depthA === -1) {
return -1;
} else if (depthB === -1) {
return 1;
} else {
return depthA > depthB ? -1 : 1;
}
}
}

export function mergeInlineCss(
acc: Array<StylesheetAsset>,
current: StylesheetAsset
): Array<StylesheetAsset> {
const lastAdded = acc.at(acc.length - 1);
const lastWasInline = lastAdded?.type === 'inline';
const currentIsInline = current?.type === 'inline';
if (lastWasInline && currentIsInline) {
const merged = { type: 'inline' as const, content: lastAdded.content + current.content };
acc[acc.length - 1] = merged;
return acc;
}
acc.push(current)
return acc;
}

export function isHoistedScript(internals: BuildInternals, id: string): boolean {
Expand Down
4 changes: 2 additions & 2 deletions packages/astro/src/core/build/page-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export async function collectPagesData(
component: route.component,
route,
moduleSpecifier: '',
css: new Map(),
styles: [],
propagatedStyles: new Map(),
propagatedScripts: new Map(),
hoistedScript: undefined,
Expand All @@ -76,7 +76,7 @@ export async function collectPagesData(
component: route.component,
route,
moduleSpecifier: '',
css: new Map(),
styles: [],
propagatedStyles: new Map(),
propagatedScripts: new Map(),
hoistedScript: undefined,
Expand Down
Loading

0 comments on commit 80e3d4d

Please sign in to comment.