diff --git a/.changeset/cookies-get-cached-parse.md b/.changeset/cookies-get-cached-parse.md new file mode 100644 index 000000000000..116660555683 --- /dev/null +++ b/.changeset/cookies-get-cached-parse.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +perf: cache the default cookie header parse and avoid allocations in `cookies.get` diff --git a/packages/kit/src/runtime/server/cookie.js b/packages/kit/src/runtime/server/cookie.js index beec1b837bff..97e1fecb6b5d 100644 --- a/packages/kit/src/runtime/server/cookie.js +++ b/packages/kit/src/runtime/server/cookie.js @@ -43,6 +43,25 @@ export function get_cookies(request, url) { parseCookie(header, { decode: (value) => value }) ); + /** @type {ReturnType | undefined} */ + let default_cookies; + + /** + * The header never changes during the request, so the default-decode parse is cached + * @param {import('cookie').ParseOptions} [opts] + */ + function parse_header(opts) { + return opts?.decode ? parseCookie(header, opts) : (default_cookies ??= parseCookie(header)); + } + + /** @param {import('./page/types.js').Cookie} cookie */ + function matches_url(cookie) { + return ( + domain_matches(url.hostname, cookie.options.domain) && + path_matches(url.pathname, cookie.options.path) + ); + } + /** @type {string | undefined} */ let normalized_url; @@ -66,22 +85,23 @@ export function get_cookies(request, url) { get(name, opts) { // Look for the most specific matching cookie from new_cookies - const best_match = Array.from(new_cookies.values()) - .filter((c) => { - return ( - c.name === name && - domain_matches(url.hostname, c.options.domain) && - path_matches(url.pathname, c.options.path) - ); - }) - .sort((a, b) => b.options.path.length - a.options.path.length)[0]; + /** @type {import('./page/types.js').Cookie | undefined} */ + let best_match; + for (const c of new_cookies.values()) { + if ( + c.name === name && + matches_url(c) && + (!best_match || c.options.path.length > best_match.options.path.length) + ) { + best_match = c; + } + } if (best_match) { return best_match.options.maxAge === 0 ? undefined : best_match.value; } - const req_cookies = parseCookie(header, { decode: opts?.decode }); - const cookie = req_cookies[name]; // the decoded string or undefined + const cookie = parse_header(opts)[name]; // the decoded string or undefined // in development, if the cookie was set during this session with `cookies.set`, // but at a different path, warn the user. (ignore cookies from request headers, @@ -104,16 +124,14 @@ export function get_cookies(request, url) { }, getAll(opts) { - const cookies = parseCookie(header, { decode: opts?.decode }); + // copy, so the cached parse isn't mutated below + const cookies = { ...parse_header(opts) }; // Group cookies by name and find the most specific one for each name const lookup = new Map(); for (const c of new_cookies.values()) { - if ( - domain_matches(url.hostname, c.options.domain) && - path_matches(url.pathname, c.options.path) - ) { + if (matches_url(c)) { const existing = lookup.get(c.name); // If no existing cookie or this one has a more specific (longer) path, use this one diff --git a/packages/kit/src/runtime/server/cookie.spec.js b/packages/kit/src/runtime/server/cookie.spec.js index 57c65642cbf3..5e34be5ffd5b 100644 --- a/packages/kit/src/runtime/server/cookie.spec.js +++ b/packages/kit/src/runtime/server/cookie.spec.js @@ -212,6 +212,14 @@ describe.skipIf(process.env.NODE_ENV !== 'production')('cookies in prod', () => ]); }); + test('get with a custom decode is not served from the cached default parse', () => { + const { cookies } = cookies_setup({ headers: { cookie: 'enc=hello%20world' } }); + + expect(cookies.get('enc')).toEqual('hello world'); + expect(cookies.get('enc', { decode: (value) => value })).toEqual('hello%20world'); + expect(cookies.get('enc')).toEqual('hello world'); + }); + test("set_internal isn't affected by defaults", () => { const { cookies, new_cookies, set_internal } = cookies_setup({ href: 'https://example.com/a/b/c'