Repository: unjs/h3
Pull request #841 (2024-07-22 11:48:06)

perf: fast path to access `searchParams`
Followup on #840

Creating a new `URL` for accessing query params has a cost too. This PR partially uses the search params segment of URL (which is already likely predicted by `pathname` fastpath too) 

---

Note: It adds `event.searchParams` mostly to be consistent with naming of web APIs and be similar to `event.url.searchParams`. DX-wise we might expose something more user friendly later or remove this before stable release aim of this PR is not DX but iterate faster on perf side of the story.


Patch:
diff --git a/package.json b/package.json
index 34e0400..a4430b0 100644
--- a/package.json
+++ b/package.json
@@ -62,7 +62,7 @@
     "express": "^4.19.2",
     "get-port": "^7.1.0",
     "get-port-please": "^3.1.2",
-    "h3-nightly": "npm:h3-nightly@2.0.0-1721647386.bd7831b",
+    "h3-nightly": "npm:h3-nightly@2.0.0-1721648175.86e2b90",
     "h3-v1": "npm:h3@^1.12.0",
     "jiti": "2.0.0-beta.3",
     "listhen": "^1.7.2",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index ecd2cb1..3cbdbd2 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -73,8 +73,8 @@ importers:
         specifier: ^3.1.2
         version: 3.1.2
       h3-nightly:
-        specifier: npm:h3-nightly@2.0.0-1721647386.bd7831b
-        version: 2.0.0-1721647386.bd7831b(crossws@0.2.4)
+        specifier: npm:h3-nightly@2.0.0-1721648175.86e2b90
+        version: 2.0.0-1721648175.86e2b90(crossws@0.2.4)
       h3-v1:
         specifier: npm:h3@^1.12.0
         version: h3@1.12.0
@@ -1735,8 +1735,8 @@ packages:
   graphemer@1.4.0:
     resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
 
-  h3-nightly@2.0.0-1721647386.bd7831b:
-    resolution: {integrity: sha512-YbSKtmhhJYr1p36iWSaeLabGwonLfbC24SbO/xQw7wTgjo/zF51Prcw4yZzqpJnxXNyQz4X7i8pIL10olG1Y+w==}
+  h3-nightly@2.0.0-1721648175.86e2b90:
+    resolution: {integrity: sha512-Ije/JE36OJhhyBP9ikwNufovleC7A7B4kvIHV5/w3u8epxZLeKpSmT/C6B/GgjvMq5UQ+5XltyIl+LKGY3f2XA==}
     peerDependencies:
       crossws: ^0.2.4
     peerDependenciesMeta:
@@ -4906,7 +4906,7 @@ snapshots:
 
   graphemer@1.4.0: {}
 
-  h3-nightly@2.0.0-1721647386.bd7831b(crossws@0.2.4):
+  h3-nightly@2.0.0-1721648175.86e2b90(crossws@0.2.4):
     dependencies:
       cookie-es: 1.2.1
       rou3: 0.4.0
diff --git a/src/adapters/node/event.ts b/src/adapters/node/event.ts
index dadb99d..f227ee0 100644
--- a/src/adapters/node/event.ts
+++ b/src/adapters/node/event.ts
@@ -34,6 +34,10 @@ export const NodeEvent = /* @__PURE__ */ (() =>
       return this.url.pathname;
     }
 
+    get searchParams(): URLSearchParams {
+      return this.url.searchParams;
+    }
+
     get method(): HTTPMethod {
       return this.node.req.method as HTTPMethod;
     }
diff --git a/src/adapters/web/event.ts b/src/adapters/web/event.ts
index e79a8cd..11856ee 100644
--- a/src/adapters/web/event.ts
+++ b/src/adapters/web/event.ts
@@ -7,6 +7,8 @@ export class WebEvent extends BaseEvent implements H3Event {
   response: H3EventResponse;
 
   _url?: URL;
+  _urlQindex?: number;
+  _searchParams?: URLSearchParams;
 
   constructor(request: Request, context?: H3EventContext) {
     super(context);
@@ -17,6 +19,7 @@ export class WebEvent extends BaseEvent implements H3Event {
   get url() {
     if (!this._url) {
       this._url = new URL(this.request.url);
+      this._searchParams = undefined;
     }
     return this._url;
   }
@@ -34,9 +37,24 @@ export class WebEvent extends BaseEvent implements H3Event {
     if (pIndex === -1) {
       return this.url.pathname; // deoptimize
     }
-    const qIndex = url.indexOf("?", pIndex);
+    const qIndex = (this._urlQindex = url.indexOf("?", pIndex));
     return url.slice(pIndex, qIndex === -1 ? undefined : qIndex);
   }
+
+  get searchParams() {
+    if (this._url) {
+      return this._url.searchParams; // reuse parsed URL
+    }
+    if (this._searchParams) {
+      return this._searchParams;
+    }
+    if (this._urlQindex === undefined) {
+      return this.url.searchParams; // deoptimize (mostly unlikely)
+    }
+    const search = this.request.url.slice(this._urlQindex);
+    this._searchParams = new URLSearchParams(search);
+    return this._searchParams;
+  }
 }
 
 class WebEventResponse implements H3EventResponse {
diff --git a/src/h3.ts b/src/h3.ts
index 59492a1..9daaf91 100644
--- a/src/h3.ts
+++ b/src/h3.ts
@@ -121,7 +121,7 @@ class _H3 implements H3 {
   }
 
   _handler(event: H3Event) {
-    const pathname = event.url.pathname;
+    const pathname = event.pathname;
 
     let _chain: Promise<unknown> | undefined;
 
diff --git a/src/types/event.ts b/src/types/event.ts
index c9b9a1e..b8aa5dd 100644
--- a/src/types/event.ts
+++ b/src/types/event.ts
@@ -17,6 +17,7 @@ export interface H3Event<
   readonly method: HTTPMethod;
   readonly path: string;
   readonly pathname: string;
+  readonly searchParams: URLSearchParams;
   readonly url: URL;
   readonly headers: Headers;
   readonly request: Request;


Test patch:
diff --git a/test/bench/impl.ts b/test/bench/impl.ts
index a4374e4..af52bc4 100644
--- a/test/bench/impl.ts
+++ b/test/bench/impl.ts
@@ -21,7 +21,7 @@ export function h3(lib: typeof _h3src) {
   // [GET] /id/:id
   app.get("/id/:id", (event) => {
     event.response.setHeader("x-powered-by", "benchmark");
-    return `${event.context.params!.id} ${event.url.searchParams.get("name")}`;
+    return `${event.context.params!.id} ${event.searchParams.get("name")}`;
   });
 
   // [POST] /json