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

experimental: css inlining #72195

Merged
merged 9 commits into from
Nov 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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[]
huozhi marked this conversation as resolved.
Show resolved Hide resolved
}
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
Loading