Skip to content

Commit

Permalink
experimental: css inlining (#72195)
Browse files Browse the repository at this point in the history
  • Loading branch information
gaojude authored and wyattjoh committed Nov 28, 2024
1 parent d4e4953 commit d56b892
Show file tree
Hide file tree
Showing 21 changed files with 234 additions and 95 deletions.
1 change: 1 addition & 0 deletions crates/next-api/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1190,6 +1190,7 @@ impl AppEndpoint {
ssr_chunking_context,
this.app_project.project().next_config(),
runtime,
this.app_project.project().next_mode(),
)
.to_resolved()
.await?;
Expand Down
1 change: 1 addition & 0 deletions crates/next-core/src/next_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,7 @@ pub struct ExperimentalConfig {
fully_specified: Option<bool>,
gzip_size: Option<bool>,

pub inline_css: Option<bool>,
instrumentation_hook: Option<bool>,
client_trace_metadata: Option<Vec<String>>,
large_page_data_bytes: Option<f64>,
Expand Down
62 changes: 45 additions & 17 deletions crates/next-core/src/next_manifests/client_reference_manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use turbo_rcstr::RcStr;
use turbo_tasks::{FxIndexSet, TryJoinIterExt, Value, ValueToString, Vc};
use turbo_tasks_fs::{File, FileSystemPath};
use turbopack_core::{
asset::AssetContent,
asset::{Asset, AssetContent},
chunk::{
availability_info::AvailabilityInfo, ChunkItem, ChunkItemExt, ChunkableModule,
ChunkingContext, ModuleId as TurbopackModuleId,
Expand All @@ -14,8 +14,9 @@ use turbopack_core::{
};
use turbopack_ecmascript::utils::StringifyJs;

use super::{ClientReferenceManifest, ManifestNode, ManifestNodeEntry, ModuleId};
use super::{ClientReferenceManifest, CssResource, ManifestNode, ManifestNodeEntry, ModuleId};
use crate::{
mode::NextMode,
next_app::ClientReferencesChunks,
next_client_reference::{ClientReferenceGraphResult, ClientReferenceType},
next_config::NextConfig,
Expand All @@ -37,6 +38,7 @@ impl ClientReferenceManifest {
ssr_chunking_context: Option<Vc<Box<dyn ChunkingContext>>>,
next_config: Vc<NextConfig>,
runtime: NextRuntime,
mode: Vc<NextMode>,
) -> Result<Vc<Box<dyn OutputAsset>>> {
let mut entry_manifest: ClientReferenceManifest = Default::default();
let mut references = FxIndexSet::default();
Expand Down Expand Up @@ -241,40 +243,66 @@ impl ClientReferenceManifest {
for (server_component, client_chunks) in
client_references_chunks.layout_segment_client_chunks.iter()
{
let client_chunks = &client_chunks.await?;

let client_chunks_paths = client_chunks
.iter()
.map(|chunk| chunk.ident().path())
.try_join()
.await?;

let server_component_name = server_component
.server_path()
.with_extension("".into())
.to_string()
.await?;

let entry_css_files = entry_manifest
.entry_css_files
.entry(server_component_name.clone_value())
.or_default();

let mut entry_css_files_with_chunk = Vec::new();
let entry_js_files = entry_manifest
.entry_js_files
.entry(server_component_name.clone_value())
.or_default();

for chunk_path in client_chunks_paths {
let client_chunks = &client_chunks.await?;
let client_chunks_with_path = client_chunks
.iter()
.map(|chunk| async move { Ok((chunk, chunk.ident().path().await?)) })
.try_join()
.await?;

for (chunk, chunk_path) in client_chunks_with_path {
if let Some(path) = client_relative_path.get_path_to(&chunk_path) {
let path = path.into();
if chunk_path.extension_ref() == Some("css") {
entry_css_files.insert(path);
entry_css_files_with_chunk.push((path, chunk));
} else {
entry_js_files.insert(path);
}
}
}

let inlined = next_config.await?.experimental.inline_css.unwrap_or(false)
&& mode.await?.is_production();
let entry_css_files_vec = entry_css_files_with_chunk
.into_iter()
.map(|(path, chunk)| async {
let content = if inlined {
if let Some(content_file) =
chunk.content().file_content().await?.as_content()
{
Some(content_file.content().to_str()?.into())
} else {
Some("".into())
}
} else {
None
};
Ok(CssResource {
path,
inlined,
content,
})
})
.try_join()
.await?;

let entry_css_files = entry_manifest
.entry_css_files
.entry(server_component_name.clone_value())
.or_default();
entry_css_files.extend(entry_css_files_vec);
}

let client_reference_manifest_json = serde_json::to_string(&entry_manifest).unwrap();
Expand Down
10 changes: 9 additions & 1 deletion crates/next-core/src/next_manifests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -355,12 +355,20 @@ pub struct ClientReferenceManifest {
pub edge_rsc_module_mapping: HashMap<ModuleId, ManifestNode>,
/// Mapping of server component path to required CSS client chunks.
#[serde(rename = "entryCSSFiles")]
pub entry_css_files: HashMap<RcStr, FxIndexSet<RcStr>>,
pub entry_css_files: HashMap<RcStr, FxIndexSet<CssResource>>,
/// Mapping of server component path to required JS client chunks.
#[serde(rename = "entryJSFiles")]
pub entry_js_files: HashMap<RcStr, FxIndexSet<RcStr>>,
}

#[derive(Serialize, Debug, Clone, Eq, Hash, PartialEq)]
pub struct CssResource {
pub path: RcStr,
pub inlined: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<RcStr>,
}

#[derive(Serialize, Default, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ModuleLoading {
Expand Down
1 change: 1 addition & 0 deletions packages/next/src/build/webpack-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1895,6 +1895,7 @@ export default async function getBaseWebpackConfig(
? new ClientReferenceManifestPlugin({
dev,
appDir,
experimentalInlineCss: !!config.experimental.inlineCss,
})
: new FlightClientEntryPlugin({
appDir,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import type { ModuleInfo } from './flight-client-entry-plugin'
interface Options {
dev: boolean
appDir: string
experimentalInlineCss: boolean
}

/**
Expand Down Expand Up @@ -83,6 +84,19 @@ export interface ClientReferenceManifestForRsc {
}
}

export type CssResource = InlinedCssFile | UninlinedCssFile

interface InlinedCssFile {
path: string
inlined: true
content: string
}

interface UninlinedCssFile {
path: string
inlined: false
}

export interface ClientReferenceManifest extends ClientReferenceManifestForRsc {
readonly moduleLoading: {
prefix: string
Expand All @@ -95,7 +109,7 @@ export interface ClientReferenceManifest extends ClientReferenceManifestForRsc {
[moduleId: string]: ManifestNode
}
entryCSSFiles: {
[entry: string]: string[]
[entry: string]: CssResource[]
}
entryJSFiles?: {
[entry: string]: string[]
Expand Down Expand Up @@ -200,11 +214,13 @@ export class ClientReferenceManifestPlugin {
dev: Options['dev'] = false
appDir: Options['appDir']
appDirBase: string
experimentalInlineCss: Options['experimentalInlineCss']

constructor(options: Options) {
this.dev = options.dev
this.appDir = options.appDir
this.appDirBase = path.dirname(this.appDir) + path.sep
this.experimentalInlineCss = options.experimentalInlineCss
}

apply(compiler: webpack.Compiler) {
Expand Down Expand Up @@ -296,9 +312,29 @@ export class ClientReferenceManifestPlugin {
/[\\/]/g,
path.sep
)

manifest.entryCSSFiles[chunkEntryName] = entrypoint
.getFiles()
.filter((f) => !f.startsWith('static/css/pages/') && f.endsWith('.css'))
.map((file) => {
const source = compilation.assets[file].source()
if (
this.experimentalInlineCss &&
// Inline CSS currently does not work properly with HMR, so we only
// inline CSS in production.
!this.dev
) {
return {
inlined: true,
path: file,
content: typeof source === 'string' ? source : source.toString(),
}
}
return {
inlined: false,
path: file,
}
})

const requiredChunks = getAppPathRequiredChunks(entrypoint, rootMainFiles)
const recordModule = (modId: ModuleId, mod: webpack.NormalModule) => {
Expand Down
1 change: 1 addition & 0 deletions packages/next/src/export/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,7 @@ async function exportAppImpl(
expireTime: nextConfig.expireTime,
after: nextConfig.experimental.after ?? false,
dynamicIO: nextConfig.experimental.dynamicIO ?? false,
inlineCss: nextConfig.experimental.inlineCss ?? false,
},
reactMaxHeadersLength: nextConfig.reactMaxHeadersLength,
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { getLinkAndScriptTags } from './get-css-inlined-link-tags'
import type { AppRenderContext } from './app-render'
import { getAssetQueryString } from './get-asset-query-string'
import { encodeURIPath } from '../../shared/lib/encode-uri-path'
import { renderCssResource } from './render-css-resource'

export async function createComponentStylesAndScripts({
filePath,
Expand All @@ -18,40 +19,14 @@ export async function createComponentStylesAndScripts({
injectedJS: Set<string>
ctx: AppRenderContext
}): Promise<[React.ComponentType<any>, React.ReactNode, React.ReactNode]> {
const { styles: cssHrefs, scripts: jsHrefs } = getLinkAndScriptTags(
const { styles: entryCssFiles, scripts: jsHrefs } = getLinkAndScriptTags(
ctx.clientReferenceManifest,
filePath,
injectedCSS,
injectedJS
)

const styles = cssHrefs
? cssHrefs.map((href, index) => {
const fullHref = `${ctx.assetPrefix}/_next/${encodeURIPath(
href
)}${getAssetQueryString(ctx, true)}`

// `Precedence` is an opt-in signal for React to handle resource
// loading and deduplication, etc. It's also used as the key to sort
// resources so they will be injected in the correct order.
// During HMR, it's critical to use different `precedence` values
// for different stylesheets, so their order will be kept.
// https://github.com/facebook/react/pull/25060
const precedence =
process.env.NODE_ENV === 'development' ? 'next_' + href : 'next'

return (
<link
rel="stylesheet"
href={fullHref}
// @ts-ignore
precedence={precedence}
crossOrigin={ctx.renderOpts.crossOrigin}
key={`style-${index}`}
/>
)
})
: null
const styles = renderCssResource(entryCssFiles, ctx)

const scripts = jsHrefs
? jsHrefs.map((href, index) => (
Expand Down
17 changes: 10 additions & 7 deletions packages/next/src/server/app-render/get-css-inlined-link-tags.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import type { ClientReferenceManifest } from '../../build/webpack/plugins/flight-manifest-plugin'
import type {
ClientReferenceManifest,
CssResource,
} from '../../build/webpack/plugins/flight-manifest-plugin'
import type { DeepReadonly } from '../../shared/lib/deep-readonly'

/**
Expand All @@ -10,9 +13,9 @@ export function getLinkAndScriptTags(
injectedCSS: Set<string>,
injectedScripts: Set<string>,
collectNewImports?: boolean
): { styles: string[]; scripts: string[] } {
): { styles: CssResource[]; scripts: string[] } {
const filePathWithoutExt = filePath.replace(/\.[^.]+$/, '')
const cssChunks = new Set<string>()
const cssChunks = new Set<CssResource>()
const jsChunks = new Set<string>()

const entryCSSFiles =
Expand All @@ -21,12 +24,12 @@ export function getLinkAndScriptTags(
clientReferenceManifest.entryJSFiles?.[filePathWithoutExt] ?? []

if (entryCSSFiles) {
for (const file of entryCSSFiles) {
if (!injectedCSS.has(file)) {
for (const css of entryCSSFiles) {
if (!injectedCSS.has(css.path)) {
if (collectNewImports) {
injectedCSS.add(file)
injectedCSS.add(css.path)
}
cssChunks.add(file)
cssChunks.add(css)
}
}
}
Expand Down
43 changes: 2 additions & 41 deletions packages/next/src/server/app-render/get-layer-assets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { AppRenderContext } from './app-render'
import { getAssetQueryString } from './get-asset-query-string'
import { encodeURIPath } from '../../shared/lib/encode-uri-path'
import type { PreloadCallbacks } from './types'
import { renderCssResource } from './render-css-resource'

export function getLayerAssets({
ctx,
Expand Down Expand Up @@ -72,47 +73,7 @@ export function getLayerAssets({
}
}

const styles = styleTags
? styleTags.map((href, index) => {
// In dev, Safari and Firefox will cache the resource during HMR:
// - https://github.com/vercel/next.js/issues/5860
// - https://bugs.webkit.org/show_bug.cgi?id=187726
// Because of this, we add a `?v=` query to bypass the cache during
// development. We need to also make sure that the number is always
// increasing.
const fullHref = `${ctx.assetPrefix}/_next/${encodeURIPath(
href
)}${getAssetQueryString(ctx, true)}`

// `Precedence` is an opt-in signal for React to handle resource
// loading and deduplication, etc. It's also used as the key to sort
// resources so they will be injected in the correct order.
// During HMR, it's critical to use different `precedence` values
// for different stylesheets, so their order will be kept.
// https://github.com/facebook/react/pull/25060
const precedence =
process.env.NODE_ENV === 'development' ? 'next_' + href : 'next'

preloadCallbacks.push(() => {
ctx.componentMod.preloadStyle(
fullHref,
ctx.renderOpts.crossOrigin,
ctx.nonce
)
})
return (
<link
rel="stylesheet"
href={fullHref}
// @ts-ignore
precedence={precedence}
crossOrigin={ctx.renderOpts.crossOrigin}
key={index}
nonce={ctx.nonce}
/>
)
})
: []
const styles = renderCssResource(styleTags, ctx, preloadCallbacks)

const scripts = scriptTags
? scriptTags.map((href, index) => {
Expand Down
Loading

0 comments on commit d56b892

Please sign in to comment.