GeezerVisonThe World Wide Web, restored to 800 pixels of glory. | ||||||||||||||||
| Home|About|Gaming cabinet|Archives|How it works|Starter sites | ||||||||||||||||
GeezerVison is showing github.com — Proposal: `103 Early Hints` from `@angular/ssr/node`, with measurements · Issue #33819 · angular/angular-cli Open Labels area: @angular/ssrgemini-triagedLabel noting that an issue has been triaged by geminiLabel noting that an issue has been triaged by geminiseverity4: memory/performance Milestone DescriptionCommandbuild DescriptionIn #26484, @alan-agius4 wrote:
The next comment asked whether any experiments had been done, and the thread was auto-locked before anyone answered. The problem. During SSR the browser is idle. It has asked for a document, and it will not learn that the page needs The measurement. An Angular 22 application, server-side rendered, over HTTP/2, driven by headless Chrome through the DevTools Protocol. 226 KB bundle, 35 KB document, server paced to about 1.6 Mbps. The number is time to hydration, taken server-side: the app asks the server for an endpoint from
The shape is the finding, more than any single number. With Early Hints the hydration time does not move: Two results worth stating because they bound the claim:
On the concern in #26484. The measurement does not dismiss it. Describe the solution you'd likeA method on app.use((req, res, next) => { angularApp.writeEarlyHints(req, res).catch(() => {}); angularApp .handle(req) .then((response) => (response ? writeResponseToNodeResponse(response, res) : next())) .catch(next); }); It writes nothing and returns The links come from the rendered document itself, parsed once per process and memoised.
HTTP/2 only. Browsers only act on a This is the implementation I tried (it also can works for the user without requiring any modifications to the framework.). early-hints.ts /** * Matches the `<link>` elements of the document, so their `rel` and `href` can be read. */ const LINK_ELEMENT_RE = /<link\b[^>]*>/g; /** * Matches the `<script>` elements that load an external file. */ const SCRIPT_ELEMENT_RE = /<script\b([^>]*\bsrc=("|')(.*?)\2[^>]*)>/g; /** * Reads an attribute out of a matched start tag. */ function attribute(tag: string, name: string): string | undefined { const match = tag.match(new RegExp(`\\b${name}=("|')(.*?)\\1`)); return match?.[2]; } /** * Resolves a document-relative href against the base href, so the value in the header does not * depend on the path the request happened to arrive on. */ function resolveHref(href: string, baseHref: string): string | undefined { if (/^[a-z][a-z0-9+.-]*:/i.test(href) || href.startsWith('//')) { // an absolute url: usable as it is, and the only kind worth a `preconnect` return href; } if (href.startsWith('/')) { return href; } const base = baseHref.endsWith('/') ? baseHref : `${baseHref}/`; return `${base.startsWith('/') ? '' : '/'}${base}${href}`.replace(/\/{2,}/g, '/'); } /** * Builds the `Link` header values for a `103 Early Hints` response out of the rendered document. */ export function extractEarlyHintLinks(html: string, baseHref: string): string[] { const links = new Set<string>(); for (const [tag] of html.matchAll(LINK_ELEMENT_RE)) { const rel = attribute(tag, 'rel'); const href = attribute(tag, 'href'); if (!href) { continue; } const resolved = resolveHref(href, baseHref); if (!resolved) { continue; } if (rel === 'preconnect') { links.add(`<${resolved}>; rel=preconnect`); } else if (rel === 'stylesheet' && attribute(tag, 'media') !== 'print') { links.add(`<${resolved}>; rel=preload; as=style`); } else if (rel === 'modulepreload') { links.add(`<${resolved}>; rel=preload; as=script; crossorigin`); } } for (const [, attributes, , src] of html.matchAll(SCRIPT_ELEMENT_RE)) { const resolved = resolveHref(src, baseHref); if (!resolved) { continue; } const isModule = attribute(attributes, 'type') === 'module'; links.add(`<${resolved}>; rel=preload; as=script${isModule ? '; crossorigin' : ''}`); } return [...links]; } app-engine.ts export class AngularAppEngine { // .... async getEarlyHintLinks(request: Request): Promise<readonly string[]> { const serverApp = await this.getAngularServerAppForRequest(request); return serverApp ? serverApp.getEarlyHintLinks() : []; } app.ts async handle(request: Request, requestContext?: unknown): Promise<Response | null> { // .... getEarlyHintLinks(): Promise<readonly string[]> { this.earlyHintLinks ??= this.assets .getIndexServerHtml() .text() .then((html) => extractEarlyHintLinks(html, this.manifest.baseHref)); return this.earlyHintLinks; } node/src/app-engine.ts async writeEarlyHints( request: IncomingMessage | Http2ServerRequest | Request, response: ServerResponse | Http2ServerResponse, ): Promise<boolean> { // HTTP/2 only: `stream` is what distinguishes an `Http2ServerResponse`, and it is also what // `writeResponseToNodeResponse` uses to tell the two apart. if (!('stream' in response)) { return false; } if (response.headersSent || response.writableEnded || response.destroyed) { return false; } const webRequest = request instanceof Request ? request : createWebRequestFromNodeRequest(request, this.trustProxyHeaders); const link = await this.angularAppEngine.getEarlyHintLinks(webRequest); if (link.length === 0 || response.headersSent || response.writableEnded) { return false; } response.writeEarlyHints({ link: link as string[] }); return true; } Describe alternatives you've considered
Activity
MetadataMetadataAssigneesLabelsarea: @angular/ssrgemini-triagedLabel noting that an issue has been triaged by geminiLabel noting that an issue has been triaged by geminiseverity4: memory/performance TypeProjectsMilestoneRelationshipsDevelopmentIssue actions | ||||||||||||||||
| You are visitor 01402503 — GeezerVison does not store the pages you look up. Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required. |