From f24ab2ddef565341746d19588e458ab02a9fe577 Mon Sep 17 00:00:00 2001 From: patak Date: Fri, 12 Jan 2024 11:00:46 +0100 Subject: [PATCH 1/4] perf: middleware to short-circuit on 304 --- packages/vite/src/node/server/index.ts | 7 +++- .../src/node/server/middlewares/transform.ts | 36 ++++++++++++------- packages/vite/src/node/server/moduleGraph.ts | 25 +++++++++++++ .../vite/src/node/server/transformRequest.ts | 12 +++---- 4 files changed, 59 insertions(+), 21 deletions(-) diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index 2501e1fde26170..674e3f9557f083 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -56,7 +56,10 @@ import { createWebSocketServer } from './ws' import { baseMiddleware } from './middlewares/base' import { proxyMiddleware } from './middlewares/proxy' import { htmlFallbackMiddleware } from './middlewares/htmlFallback' -import { transformMiddleware } from './middlewares/transform' +import { + cachedTransformMiddleware, + transformMiddleware, +} from './middlewares/transform' import { createDevHtmlTransformFn, indexHtmlMiddleware, @@ -731,6 +734,8 @@ export async function _createServer( middlewares.use(timeMiddleware(root)) } + middlewares.use(cachedTransformMiddleware(server)) + // cors (enabled by default) const { cors } = serverConfig if (cors !== false) { diff --git a/packages/vite/src/node/server/middlewares/transform.ts b/packages/vite/src/node/server/middlewares/transform.ts index 12e2fcef9739ec..c901e3deb3b3b8 100644 --- a/packages/vite/src/node/server/middlewares/transform.ts +++ b/packages/vite/src/node/server/middlewares/transform.ts @@ -44,6 +44,30 @@ const debugCache = createDebugger('vite:cache') const knownIgnoreList = new Set(['/', '/favicon.ico']) +/** + * A middleware that short-circuits the middleware chain to serve cached transformed modules + */ +export function cachedTransformMiddleware( + server: ViteDevServer, +): Connect.NextHandleFunction { + // Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...` + return function viteCachedTransformMiddleware(req, res, next) { + // check if we can return 304 early + const ifNoneMatch = req.headers['if-none-match'] + if ( + ifNoneMatch && + server.moduleGraph.getModuleByEtag(ifNoneMatch)?.transformResult?.etag === + ifNoneMatch + ) { + debugCache?.(`[304] ${prettifyUrl(req.url!, server.config.root)}`) + res.statusCode = 304 + return res.end() + } + + next() + } +} + export function transformMiddleware( server: ViteDevServer, ): Connect.NextHandleFunction { @@ -155,18 +179,6 @@ export function transformMiddleware( url = injectQuery(url, 'direct') } - // check if we can return 304 early - const ifNoneMatch = req.headers['if-none-match'] - if ( - ifNoneMatch && - (await server.moduleGraph.getModuleByUrl(url, false))?.transformResult - ?.etag === ifNoneMatch - ) { - debugCache?.(`[304] ${prettifyUrl(url, server.config.root)}`) - res.statusCode = 304 - return res.end() - } - // resolve, load and transform using the plugin container const result = await transformRequest(url, server, { html: req.headers.accept?.includes('text/html'), diff --git a/packages/vite/src/node/server/moduleGraph.ts b/packages/vite/src/node/server/moduleGraph.ts index 435b3876dde3ab..40efa65f32b67e 100644 --- a/packages/vite/src/node/server/moduleGraph.ts +++ b/packages/vite/src/node/server/moduleGraph.ts @@ -88,6 +88,7 @@ export type ResolvedUrl = [ export class ModuleGraph { urlToModuleMap = new Map() idToModuleMap = new Map() + etagToModuleMap = new Map() // a single file may corresponds to multiple modules with different queries fileToModulesMap = new Map>() safeModulesPath = new Set() @@ -192,6 +193,9 @@ export class ModuleGraph { // Don't invalidate mod.info and mod.meta, as they are part of the processing pipeline // Invalidating the transform result is enough to ensure this module is re-processed next time it is requested + const etag = mod.transformResult?.etag + if (etag) this.etagToModuleMap.delete(etag) + mod.transformResult = null mod.ssrTransformResult = null mod.ssrModule = null @@ -419,6 +423,27 @@ export class ModuleGraph { return this._resolveUrl(url, ssr) } + updateModuleTransformResult( + mod: ModuleNode, + result: TransformResult | null, + ssr: boolean, + ): void { + if (ssr) { + mod.ssrTransformResult = result + } else { + const prevEtag = mod.transformResult?.etag + if (prevEtag) this.etagToModuleMap.delete(prevEtag) + + mod.transformResult = result + + if (result?.etag) this.etagToModuleMap.set(result.etag, mod) + } + } + + getModuleByEtag(etag: string): ModuleNode | undefined { + return this.etagToModuleMap.get(etag) + } + /** * @internal */ diff --git a/packages/vite/src/node/server/transformRequest.ts b/packages/vite/src/node/server/transformRequest.ts index e3ce47fc1a86fa..a6fce5d6634d39 100644 --- a/packages/vite/src/node/server/transformRequest.ts +++ b/packages/vite/src/node/server/transformRequest.ts @@ -361,10 +361,8 @@ async function loadAndTransform( // Only cache the result if the module wasn't invalidated while it was // being processed, so it is re-processed next time if it is stale - if (timestamp > mod.lastInvalidationTimestamp) { - if (ssr) mod.ssrTransformResult = result - else mod.transformResult = result - } + if (timestamp > mod.lastInvalidationTimestamp) + moduleGraph.updateModuleTransformResult(mod, result, ssr) return result } @@ -465,10 +463,8 @@ async function handleModuleSoftInvalidation( // Only cache the result if the module wasn't invalidated while it was // being processed, so it is re-processed next time if it is stale - if (timestamp > mod.lastInvalidationTimestamp) { - if (ssr) mod.ssrTransformResult = result - else mod.transformResult = result - } + if (timestamp > mod.lastInvalidationTimestamp) + server.moduleGraph.updateModuleTransformResult(mod, result, ssr) return result } From 1c4ec512aeaa0dce31903bb0e9c5030dd90c2ab9 Mon Sep 17 00:00:00 2001 From: patak Date: Fri, 12 Jan 2024 11:16:13 +0100 Subject: [PATCH 2/4] fix: move after cors middleware --- packages/vite/src/node/server/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index 674e3f9557f083..e1b069e49528db 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -734,14 +734,14 @@ export async function _createServer( middlewares.use(timeMiddleware(root)) } - middlewares.use(cachedTransformMiddleware(server)) - // cors (enabled by default) const { cors } = serverConfig if (cors !== false) { middlewares.use(corsMiddleware(typeof cors === 'boolean' ? {} : cors)) } + middlewares.use(cachedTransformMiddleware(server)) + // proxy const { proxy } = serverConfig if (proxy) { From 7f189df661d075da3ec0eba7034f1d8823423f18 Mon Sep 17 00:00:00 2001 From: patak Date: Fri, 12 Jan 2024 18:48:32 +0100 Subject: [PATCH 3/4] fix: mixed etag for direct CSS that is also imported --- .../src/node/server/middlewares/transform.ts | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/packages/vite/src/node/server/middlewares/transform.ts b/packages/vite/src/node/server/middlewares/transform.ts index c901e3deb3b3b8..a2afde5f16d26a 100644 --- a/packages/vite/src/node/server/middlewares/transform.ts +++ b/packages/vite/src/node/server/middlewares/transform.ts @@ -54,14 +54,21 @@ export function cachedTransformMiddleware( return function viteCachedTransformMiddleware(req, res, next) { // check if we can return 304 early const ifNoneMatch = req.headers['if-none-match'] - if ( - ifNoneMatch && - server.moduleGraph.getModuleByEtag(ifNoneMatch)?.transformResult?.etag === - ifNoneMatch - ) { - debugCache?.(`[304] ${prettifyUrl(req.url!, server.config.root)}`) - res.statusCode = 304 - return res.end() + if (ifNoneMatch) { + const moduleByEtag = server.moduleGraph.getModuleByEtag(ifNoneMatch) + if (moduleByEtag?.transformResult?.etag === ifNoneMatch) { + // For direct CSS requests, if the same CSS file is imported in a module, + // the browser sends the request for the direct CSS request with the etag + // from the imported CSS module. We ignore the etag in this case. + const mixedEtag = + !req.headers.accept?.includes('text/css') && + isDirectRequest(moduleByEtag.url) + if (!mixedEtag) { + debugCache?.(`[304] ${prettifyUrl(req.url!, server.config.root)}`) + res.statusCode = 304 + return res.end() + } + } } next() From e9a6011906edb6d9dc8417e7c560c4ee234d222c Mon Sep 17 00:00:00 2001 From: patak Date: Thu, 18 Jan 2024 18:35:20 +0100 Subject: [PATCH 4/4] fix: adding a public file with the same path as a module --- packages/vite/src/node/server/index.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index 38c5c209ad8000..1c507d1ff000fb 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -684,7 +684,17 @@ export async function _createServer( if (publicDir && publicFiles) { if (file.startsWith(publicDir)) { - publicFiles[isUnlink ? 'delete' : 'add'](file.slice(publicDir.length)) + const path = file.slice(publicDir.length) + publicFiles[isUnlink ? 'delete' : 'add'](path) + if (!isUnlink) { + const moduleWithSamePath = await moduleGraph.getModuleByUrl(path) + const etag = moduleWithSamePath?.transformResult?.etag + if (etag) { + // The public file should win on the next request over a module with the + // same path. Prevent the transform etag fast path from serving the module + moduleGraph.etagToModuleMap.delete(etag) + } + } } } await handleFileAddUnlink(file, server, isUnlink)