diff --git a/docs/api-reference/next/future/image.md b/docs/api-reference/next/future/image.md
index fc5e08c63d3d..1d694b94a1dd 100644
--- a/docs/api-reference/next/future/image.md
+++ b/docs/api-reference/next/future/image.md
@@ -15,7 +15,7 @@ description: Try the latest Image Optimization with the experimental `next/futur
The `next/future/image` component is an experiment to improve both the performance and developer experience of `next/image` by using the native `` element with better default behavior.
-This new component is considered experimental and therefore not covered by semver, and may cause unexpected or broken application behavior. This component uses web native [lazy loading](https://caniuse.com/loading-lazy-attr) and [`aspect-ratio`](https://caniuse.com/mdn-css_properties_aspect-ratio), which currently isn't supported in IE11 or before Safari 15.4.
+This new component is considered experimental and therefore not covered by semver, and may cause unexpected or broken application behavior. This component uses browser native [lazy loading](https://caniuse.com/loading-lazy-attr), which may fallback to eager loading for older browsers before Safari 15.4. When using the blur-up placeholder, older browsers before Safari 12 will fallback to empty placeholder. When using styles with `width`/`height` of `auto`, it is possible to cause [Layout Shift](https://web.dev/cls/) on older browsers before [Chrome 79](https://chromestatus.com/feature/5695266130755584), [Firefox 69](https://bugzilla.mozilla.org/show_bug.cgi?id=1547231), and [Safari 14.2](https://bugs.webkit.org/show_bug.cgi?id=201641). For more details, see [this MDN video](https://www.youtube.com/watch?v=4-d_SoCHeWE).
To use `next/future/image`, add the following to your `next.config.js` file:
diff --git a/packages/next/client/app-index.tsx b/packages/next/client/app-index.tsx
index 715f257db3d0..2e51e8277210 100644
--- a/packages/next/client/app-index.tsx
+++ b/packages/next/client/app-index.tsx
@@ -32,10 +32,13 @@ self.__next_require__ = __webpack_require__
// eslint-disable-next-line no-undef
;(self as any).__next_chunk_load__ = (chunk: string) => {
if (chunk.endsWith('.css')) {
- const link = document.createElement('link')
- link.rel = 'stylesheet'
- link.href = '/_next/' + chunk
- document.head.appendChild(link)
+ const existingTag = document.querySelector(`link[href="${chunk}"]`)
+ if (!existingTag) {
+ const link = document.createElement('link')
+ link.rel = 'stylesheet'
+ link.href = '/_next/' + chunk
+ document.head.appendChild(link)
+ }
return Promise.resolve()
}
@@ -158,19 +161,19 @@ async function loadCss(cssChunkInfoJson: string) {
return (self as any).__next_require__(data.id)
}
- return Promise.resolve(1)
+ return Promise.resolve()
}
function createLoadFlightCssStream(callback?: () => Promise) {
const cssLoadingPromises: Promise[] = []
const loadCssFromStreamData = (data: string) => {
- const seg = data.split(':')
- if (seg[0] === 'CSS') {
- const cssJson = seg.slice(1).join(':')
+ if (data.startsWith('CSS')) {
+ const cssJson = data.slice(4).trim()
if (!loadedCss.has(cssJson)) cssLoadingPromises.push(loadCss(cssJson))
}
}
+ // TODO-APP: Refine the buffering code here to make it more correct.
let buffer = ''
const loadCssFromFlight = new TransformStream({
transform(chunk, controller) {
@@ -178,13 +181,18 @@ function createLoadFlightCssStream(callback?: () => Promise) {
buffer += data
let index
while ((index = buffer.indexOf('\n')) !== -1) {
- const line = buffer.slice(0, index)
+ const line = buffer.slice(0, index + 1)
buffer = buffer.slice(index + 1)
- loadCssFromStreamData(line)
+ if (line.startsWith('CSS:')) {
+ loadCssFromStreamData(line)
+ } else {
+ controller.enqueue(new TextEncoder().encode(line))
+ }
}
- loadCssFromStreamData(buffer)
- if (!data.startsWith('CSS:')) {
- controller.enqueue(chunk)
+
+ if (buffer && !buffer.startsWith('CSS:')) {
+ controller.enqueue(new TextEncoder().encode(buffer))
+ buffer = ''
}
},
})
diff --git a/packages/next/client/components/app-router.client.tsx b/packages/next/client/components/app-router.client.tsx
index cf5eeb2ad108..9b571472108e 100644
--- a/packages/next/client/components/app-router.client.tsx
+++ b/packages/next/client/components/app-router.client.tsx
@@ -1,5 +1,5 @@
import React, { useEffect } from 'react'
-import { createFromFetch } from 'next/dist/compiled/react-server-dom-webpack'
+import { createFromReadableStream } from 'next/dist/compiled/react-server-dom-webpack'
import {
AppRouterContext,
AppTreeContext,
@@ -16,16 +16,67 @@ import {
// LayoutSegmentsContext,
} from './hooks-client-context'
-function fetchFlight(
- url: URL,
- flightRouterStateData: string
-): Promise {
+async function loadCss(cssChunkInfoJson: string) {
+ const data = JSON.parse(cssChunkInfoJson)
+ await Promise.all(
+ data.chunks.map((chunkId: string) => {
+ // load css related chunks
+ return (self as any).__next_chunk_load__(chunkId)
+ })
+ )
+ // In development mode, import css in dev when it's wrapped by style loader.
+ // In production mode, css are standalone chunk that doesn't need to be imported.
+ if (data.id) {
+ ;(self as any).__next_require__(data.id)
+ }
+}
+
+const loadCssFromStreamData = (data: string) => {
+ if (data.startsWith('CSS:')) {
+ loadCss(data.slice(4).trim())
+ }
+}
+
+function fetchFlight(url: URL, flightRouterStateData: string): ReadableStream {
const flightUrl = new URL(url)
const searchParams = flightUrl.searchParams
searchParams.append('__flight__', '1')
searchParams.append('__flight_router_state_tree__', flightRouterStateData)
- return fetch(flightUrl.toString())
+ const { readable, writable } = new TransformStream()
+
+ // TODO-APP: Refine the buffering code here to make it more correct.
+ let buffer = ''
+ const loadCssFromFlight = new TransformStream({
+ transform(chunk, controller) {
+ const data = new TextDecoder().decode(chunk)
+ buffer += data
+ let index
+ while ((index = buffer.indexOf('\n')) !== -1) {
+ const line = buffer.slice(0, index + 1)
+ buffer = buffer.slice(index + 1)
+
+ if (line.startsWith('CSS:')) {
+ loadCssFromStreamData(line)
+ } else {
+ controller.enqueue(new TextEncoder().encode(line))
+ }
+ }
+ if (buffer && !buffer.startsWith('CSS:')) {
+ controller.enqueue(new TextEncoder().encode(buffer))
+ buffer = ''
+ }
+ },
+ flush() {
+ loadCssFromStreamData(buffer)
+ },
+ })
+
+ fetch(flightUrl.toString()).then((res) => {
+ res.body?.pipeThrough(loadCssFromFlight).pipeTo(writable)
+ })
+
+ return readable
}
export function fetchServerResponse(
@@ -33,7 +84,7 @@ export function fetchServerResponse(
flightRouterState: FlightRouterState
): { readRoot: () => FlightData } {
const flightRouterStateData = JSON.stringify(flightRouterState)
- return createFromFetch(fetchFlight(url, flightRouterStateData))
+ return createFromReadableStream(fetchFlight(url, flightRouterStateData))
}
function ErrorOverlay({
@@ -56,11 +107,13 @@ let initialParallelRoutes: CacheNode['parallelRoutes'] =
export default function AppRouter({
initialTree,
initialCanonicalUrl,
+ initialStylesheets,
children,
hotReloader,
}: {
initialTree: FlightRouterState
initialCanonicalUrl: string
+ initialStylesheets: string[]
children: React.ReactNode
hotReloader?: React.ReactNode
}) {
@@ -259,6 +312,7 @@ export default function AppRouter({
// Root node always has `url`
// Provided in AppTreeContext to ensure it can be overwritten in layout-router
url: canonicalUrl,
+ stylesheets: initialStylesheets,
}}
>
{cache.subTreeData}
diff --git a/packages/next/client/components/layout-router.client.tsx b/packages/next/client/components/layout-router.client.tsx
index 92376c54ec4e..a7bcf85f3357 100644
--- a/packages/next/client/components/layout-router.client.tsx
+++ b/packages/next/client/components/layout-router.client.tsx
@@ -235,7 +235,7 @@ export default function OuterLayoutRouter({
childProp: ChildProp
loading: React.ReactNode | undefined
}) {
- const { childNodes, tree, url } = useContext(AppTreeContext)
+ const { childNodes, tree, url, stylesheets } = useContext(AppTreeContext)
let childNodesForParallelRouter = childNodes.get(parallelRouterKey)
if (!childNodesForParallelRouter) {
@@ -256,6 +256,11 @@ export default function OuterLayoutRouter({
return (
<>
+ {stylesheets
+ ? stylesheets.map((href) => (
+
+ ))
+ : null}
{preservedSegments.map((preservedSegment) => {
return (
diff --git a/packages/next/server/app-render.tsx b/packages/next/server/app-render.tsx
index 0f391dc7e9ec..bd5109398f94 100644
--- a/packages/next/server/app-render.tsx
+++ b/packages/next/server/app-render.tsx
@@ -191,11 +191,7 @@ function createServerComponentRenderer(
globalThis.__next_chunk_load__ = () => Promise.resolve()
}
- const cssFlightData = getCssFlightData(
- ComponentMod,
- serverComponentManifest,
- dev
- )
+ const cssFlightData = getCssFlightData(ComponentMod, serverComponentManifest)
let RSCStream: ReadableStream
const createRSCStream = () => {
@@ -212,7 +208,7 @@ function createServerComponentRenderer(
}
const writable = transformStream.writable
- const ServerComponentWrapper = () => {
+ return function ServerComponentWrapper() {
const reqStream = createRSCStream()
const response = useFlightResponse(
writable,
@@ -221,11 +217,8 @@ function createServerComponentRenderer(
serverComponentManifest,
cssFlightData
)
- const root = response.readRoot()
- return root
+ return response.readRoot()
}
-
- return ServerComponentWrapper
}
type DynamicParamTypes = 'catchall' | 'optional-catchall' | 'dynamic'
@@ -330,11 +323,30 @@ function getSegmentParam(segment: string): {
return null
}
-function getCssFlightData(
+function getCSSInlinedLinkTags(
ComponentMod: any,
serverComponentManifest: any,
dev: boolean
) {
+ if (dev) return []
+
+ const importedServerCSSFiles: string[] =
+ ComponentMod.__client__?.__next_rsc_css__ || []
+
+ return Array.from(
+ new Set(
+ importedServerCSSFiles
+ .map((css) =>
+ css.endsWith('.css')
+ ? serverComponentManifest[css].default.chunks
+ : []
+ )
+ .flat()
+ )
+ )
+}
+
+function getCssFlightData(ComponentMod: any, serverComponentManifest: any) {
const importedServerCSSFiles: string[] =
ComponentMod.__client__?.__next_rsc_css__ || []
@@ -342,10 +354,6 @@ function getCssFlightData(
(css) => serverComponentManifest[css].default
)
- if (dev) {
- return cssFiles.map((css) => `CSS:${JSON.stringify(css)}\n`).join('')
- }
-
// Multiple css chunks could be merged into one by mini-css-extract-plugin,
// we use a set here to dedupe the css chunks in production.
const cssSet: Set = cssFiles.reduce((res, css) => {
@@ -773,8 +781,7 @@ export async function renderToHTML(
const cssFlightData = getCssFlightData(
ComponentMod,
- serverComponentManifest,
- dev
+ serverComponentManifest
)
const flightData: FlightData = [
// TODO-APP: change walk to output without ''
@@ -800,6 +807,12 @@ export async function renderToHTML(
// /blog/[slug] /blog/hello-world -> ['children', 'blog', 'children', ['slug', 'hello-world']]
const initialTree = createFlightRouterStateFromLoaderTree(tree)
+ const initialStylesheets: string[] = getCSSInlinedLinkTags(
+ ComponentMod,
+ serverComponentManifest,
+ dev
+ )
+
const { Component: ComponentTree } = createComponentTree({
createSegmentPath: (child) => child,
tree,
@@ -825,6 +838,7 @@ export async function renderToHTML(
hotReloader={HotReloader && }
initialCanonicalUrl={initialCanonicalUrl}
initialTree={initialTree}
+ initialStylesheets={initialStylesheets}
>
@@ -905,7 +919,6 @@ export async function renderToHTML(
return await continueFromInitialStream(renderStream, {
dev,
- suffix: '',
dataStream: serverComponentsInlinedTransformStream?.readable,
generateStaticHTML: generateStaticHTML || !hasConcurrentFeatures,
flushEffectHandler,
diff --git a/packages/next/server/node-web-streams-helper.ts b/packages/next/server/node-web-streams-helper.ts
index ddbdedce2f60..6502173af9ae 100644
--- a/packages/next/server/node-web-streams-helper.ts
+++ b/packages/next/server/node-web-streams-helper.ts
@@ -201,7 +201,7 @@ export async function continueFromInitialStream(
const transforms: Array> = [
createBufferedTransformStream(),
flushEffectHandler ? createFlushEffectStream(flushEffectHandler) : null,
- suffixUnclosed != null ? createBufferedSuffix(suffixUnclosed) : null,
+ suffixUnclosed != null ? createDeferredSuffixStream(suffixUnclosed) : null,
dataStream ? createInlineDataStream(dataStream) : null,
suffixUnclosed != null ? createSuffixStream(closeTag) : null,
dev ? createDevScriptTransformStream() : null,
@@ -213,45 +213,41 @@ export async function continueFromInitialStream(
)
}
-export function createPrefixStream(
- prefix: string
+export function createSuffixStream(
+ suffix: string
): TransformStream {
- let prefixFlushed = false
return new TransformStream({
- transform(chunk, controller) {
- if (!prefixFlushed) {
- prefixFlushed = true
- controller.enqueue(encodeText(prefix))
- }
- controller.enqueue(chunk)
- },
flush(controller) {
- if (!prefixFlushed) {
- controller.enqueue(encodeText(prefix))
+ if (suffix) {
+ controller.enqueue(encodeText(suffix))
}
},
})
}
-export function createSuffixStream(
- suffix: string
+export function createPrefixStream(
+ prefix: string
): TransformStream {
+ let prefixFlushed = false
return new TransformStream({
- flush(controller) {
- if (suffix) {
- controller.enqueue(encodeText(suffix))
+ transform(chunk, controller) {
+ if (!prefixFlushed && prefix) {
+ prefixFlushed = true
+ controller.enqueue(encodeText(prefix))
}
+ controller.enqueue(chunk)
},
})
}
// Suffix after main body content - scripts before