Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PoC: feat(reg-exp-router): Introduced PreparedRegExpRouter #1796

Open
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

usualoma
Copy link
Member

@usualoma usualoma commented Dec 9, 2023

What is the PR to improve?

With this PR, we aim to improve the reduction of RegExpRouter bundle size and initial addition time.

As you can see in the code I added to the following unit test, we can prepare regular expressions, etc. in advance by passing the routing information to buildInitParams(). This can be used to simplify the initialization process at startup.

src/router/reg-exp-router/router.test.ts

Benchmark

In Node.js, it is more than 10 times faster than RegExpRouter and close to LinearRouter; in Bun, it may be faster than LinearRouter.

$ npm run bench-includes-init:node

> bench-includes-init:node
> tsx ./src/bench-includes-init.mts

cpu: Apple M2 Pro
runtime: node v20.0.0 (arm64-darwin)

benchmark                 time (avg)             (min … max)       p75       p99      p995
------------------------------------------------------------ -----------------------------
• GET /user
------------------------------------------------------------ -----------------------------
RegExpRouter           34.18 µs/iter     (26.04 µs … 1.7 ms)  31.29 µs  86.38 µs 382.17 µs
PreparedRegExpRouter    2.07 µs/iter     (1.08 µs … 8.86 µs)    1.9 µs   8.86 µs   8.86 µs
TrieRouter              6.07 µs/iter   (4.79 µs … 638.33 µs)   5.58 µs   7.63 µs   9.79 µs
LinearRouter            1.02 µs/iter   (958.27 ns … 1.07 µs)   1.04 µs   1.07 µs   1.07 µs
MedleyRouter            3.04 µs/iter     (2.93 µs … 3.25 µs)   3.07 µs   3.25 µs   3.25 µs
FindMyWay              91.65 µs/iter    (78.04 µs … 2.14 ms)  88.04 µs 143.83 µs 607.83 µs
KoaTreeRouter           2.31 µs/iter     (2.15 µs … 3.44 µs)   2.26 µs   3.44 µs   3.44 µs
TrekRouter              3.05 µs/iter     (2.96 µs … 3.11 µs)   3.07 µs   3.11 µs   3.11 µs

summary for GET /user
  LinearRouter
   2.04x faster than PreparedRegExpRouter
   2.27x faster than KoaTreeRouter
   2.99x faster than MedleyRouter
   2.99x faster than TrekRouter
   5.97x faster than TrieRouter
   33.6x faster than RegExpRouter
   90.11x faster than FindMyWay

• GET /user/comments
------------------------------------------------------------ -----------------------------
RegExpRouter            41.4 µs/iter       (26 µs … 8.62 ms)  30.79 µs    336 µs 827.21 µs
PreparedRegExpRouter    2.45 µs/iter     (1.73 µs … 6.14 µs)   2.41 µs   6.14 µs   6.14 µs
TrieRouter              6.45 µs/iter     (4.79 µs … 1.21 ms)   5.54 µs   8.83 µs     10 µs
LinearRouter            1.18 µs/iter      (1.1 µs … 1.39 µs)   1.21 µs   1.39 µs   1.39 µs
MedleyRouter            3.65 µs/iter     (3.21 µs … 9.79 µs)    3.4 µs   9.79 µs   9.79 µs
FindMyWay              99.71 µs/iter    (77.96 µs … 3.26 ms)  87.96 µs 265.92 µs   1.24 ms
KoaTreeRouter           2.41 µs/iter     (2.29 µs … 2.58 µs)   2.46 µs   2.58 µs   2.58 µs
TrekRouter              3.49 µs/iter     (3.16 µs … 4.84 µs)   3.47 µs   4.84 µs   4.84 µs

summary for GET /user/comments
  LinearRouter
   2.04x faster than KoaTreeRouter
   2.08x faster than PreparedRegExpRouter
   2.96x faster than TrekRouter
   3.09x faster than MedleyRouter
   5.47x faster than TrieRouter
   35.1x faster than RegExpRouter
   84.55x faster than FindMyWay

• GET /user/lookup/username/hey
------------------------------------------------------------ -----------------------------
RegExpRouter           58.27 µs/iter    (25.88 µs … 7.42 ms)   31.5 µs 971.42 µs      2 ms
PreparedRegExpRouter    8.07 µs/iter     (2.71 µs … 35.7 µs)    5.3 µs   35.7 µs   35.7 µs
TrieRouter               7.3 µs/iter     (4.96 µs … 5.18 ms)   5.83 µs  10.83 µs  12.71 µs
LinearRouter            1.39 µs/iter     (1.27 µs … 1.82 µs)   1.43 µs   1.82 µs   1.82 µs
MedleyRouter            4.03 µs/iter     (3.47 µs … 8.86 µs)    3.7 µs   8.86 µs   8.86 µs
FindMyWay             103.31 µs/iter    (80.46 µs … 4.16 ms)  89.54 µs 306.04 µs   1.53 ms
KoaTreeRouter           2.83 µs/iter     (2.53 µs … 4.82 µs)   2.77 µs   4.82 µs   4.82 µs
TrekRouter              4.01 µs/iter    (3.41 µs … 11.82 µs)   3.71 µs  11.82 µs  11.82 µs

summary for GET /user/lookup/username/hey
  LinearRouter
   2.03x faster than KoaTreeRouter
   2.88x faster than TrekRouter
   2.9x faster than MedleyRouter
   5.25x faster than TrieRouter
   5.8x faster than PreparedRegExpRouter
   41.85x faster than RegExpRouter
   74.2x faster than FindMyWay

• GET /event/abcd1234/comments
------------------------------------------------------------ -----------------------------
RegExpRouter           68.34 µs/iter    (26.33 µs … 8.38 ms)  30.92 µs   1.11 ms   2.45 ms
PreparedRegExpRouter    4.31 µs/iter     (2.48 µs … 7.95 µs)   4.74 µs   7.95 µs   7.95 µs
TrieRouter              7.77 µs/iter     (5.04 µs … 6.68 ms)   5.83 µs   9.42 µs  16.33 µs
LinearRouter            4.19 µs/iter    (1.33 µs … 32.59 µs)   2.43 µs  32.59 µs  32.59 µs
MedleyRouter            4.29 µs/iter       (3.5 µs … 9.6 µs)   4.06 µs    9.6 µs    9.6 µs
FindMyWay             126.04 µs/iter    (80.38 µs … 8.11 ms)   90.5 µs   1.12 ms   2.56 ms
KoaTreeRouter           2.77 µs/iter     (2.51 µs … 3.26 µs)    2.9 µs   3.26 µs   3.26 µs
TrekRouter              3.89 µs/iter      (3.65 µs … 4.4 µs)   3.98 µs    4.4 µs    4.4 µs

summary for GET /event/abcd1234/comments
  KoaTreeRouter
   1.41x faster than TrekRouter
   1.51x faster than LinearRouter
   1.55x faster than MedleyRouter
   1.56x faster than PreparedRegExpRouter
   2.81x faster than TrieRouter
   24.71x faster than RegExpRouter
   45.58x faster than FindMyWay

• POST /event/abcd1234/comment
------------------------------------------------------------ -----------------------------
RegExpRouter           82.01 µs/iter   (26.04 µs … 13.44 ms)  31.17 µs 906.29 µs   3.06 ms
PreparedRegExpRouter    6.74 µs/iter    (2.58 µs … 35.23 µs)   6.05 µs  35.23 µs  35.23 µs
TrieRouter              8.08 µs/iter    (5.04 µs … 10.08 ms)   5.83 µs   9.67 µs  16.13 µs
LinearRouter          564.22 ns/iter   (433.07 ns … 1.31 µs) 617.83 ns   1.31 µs   1.31 µs
MedleyRouter            4.39 µs/iter     (3.66 µs … 12.5 µs)   4.03 µs   12.5 µs   12.5 µs
FindMyWay             123.96 µs/iter   (78.21 µs … 13.05 ms)  87.96 µs 438.83 µs   2.23 ms
KoaTreeRouter           3.22 µs/iter     (2.52 µs … 6.81 µs)   3.35 µs   6.81 µs   6.81 µs
TrekRouter              4.21 µs/iter    (2.54 µs … 14.32 ms)   2.92 µs   6.33 µs   7.13 µs

summary for POST /event/abcd1234/comment
  LinearRouter
   5.71x faster than KoaTreeRouter
   7.46x faster than TrekRouter
   7.78x faster than MedleyRouter
   11.94x faster than PreparedRegExpRouter
   14.32x faster than TrieRouter
   145.34x faster than RegExpRouter
   219.7x faster than FindMyWay

• GET /very/deeply/nested/route/hello/there
------------------------------------------------------------ -----------------------------
RegExpRouter          100.97 µs/iter   (26.17 µs … 17.51 ms)  31.08 µs   1.21 ms   3.09 ms
PreparedRegExpRouter    5.94 µs/iter    (2.83 µs … 10.33 µs)   7.18 µs  10.33 µs  10.33 µs
TrieRouter              8.45 µs/iter    (4.96 µs … 12.71 ms)   5.71 µs  10.42 µs  19.38 µs
LinearRouter            1.65 µs/iter     (1.35 µs … 2.85 µs)   1.67 µs   2.85 µs   2.85 µs
MedleyRouter             4.1 µs/iter     (3.59 µs … 5.16 µs)   4.24 µs   5.16 µs   5.16 µs
FindMyWay             120.86 µs/iter   (77.17 µs … 15.57 ms)  86.96 µs    415 µs   2.35 ms
KoaTreeRouter           2.77 µs/iter      (2.53 µs … 3.9 µs)    2.8 µs    3.9 µs    3.9 µs
TrekRouter              3.92 µs/iter     (3.56 µs … 4.99 µs)   3.99 µs   4.99 µs   4.99 µs

summary for GET /very/deeply/nested/route/hello/there
  LinearRouter
   1.68x faster than KoaTreeRouter
   2.38x faster than TrekRouter
   2.49x faster than MedleyRouter
   3.6x faster than PreparedRegExpRouter
   5.12x faster than TrieRouter
   61.2x faster than RegExpRouter
   73.25x faster than FindMyWay

• GET /static/index.html
------------------------------------------------------------ -----------------------------
RegExpRouter           99.91 µs/iter   (26.13 µs … 18.35 ms)     31 µs   1.46 ms   3.55 ms
PreparedRegExpRouter   26.83 µs/iter    (5.25 µs … 161.2 µs)  28.99 µs  161.2 µs  161.2 µs
TrieRouter             72.71 µs/iter    (4.96 µs … 59.46 ms)   9.04 µs 258.71 µs 488.63 µs
LinearRouter            2.06 µs/iter      (1.2 µs … 5.51 µs)   2.67 µs   5.51 µs   5.51 µs
MedleyRouter             3.9 µs/iter      (3.6 µs … 4.12 µs)   3.99 µs   4.12 µs   4.12 µs
FindMyWay             125.48 µs/iter   (79.04 µs … 19.41 ms)  88.38 µs 242.96 µs    2.3 ms
KoaTreeRouter           2.84 µs/iter     (2.64 µs … 3.13 µs)   2.91 µs   3.13 µs   3.13 µs
TrekRouter              3.95 µs/iter     (3.62 µs … 4.21 µs)   4.09 µs   4.21 µs   4.21 µs

summary for GET /static/index.html
  LinearRouter
   1.38x faster than KoaTreeRouter
   1.89x faster than MedleyRouter
   1.92x faster than TrekRouter
   13.01x faster than PreparedRegExpRouter
   35.26x faster than TrieRouter
   48.46x faster than RegExpRouter
   60.86x faster than FindMyWay
$ npm run bench-includes-init:bun

> bench-includes-init:bun
> bun run ./src/bench-includes-init.mts

cpu: Apple M2 Pro
runtime: bun 1.0.12 (arm64-darwin)

benchmark                 time (avg)             (min … max)       p75       p99      p995
------------------------------------------------------------ -----------------------------
• GET /user
------------------------------------------------------------ -----------------------------
RegExpRouter           30.48 µs/iter     (23.83 µs … 1.7 ms)     30 µs  51.83 µs  66.29 µs
PreparedRegExpRouter    1.34 µs/iter  (592.65 ns … 15.25 µs) 934.77 ns  15.25 µs  15.25 µs
TrieRouter              5.82 µs/iter     (5.35 µs … 7.61 µs)    5.9 µs   7.61 µs   7.61 µs
LinearRouter            1.14 µs/iter     (1.06 µs … 1.45 µs)   1.15 µs   1.45 µs   1.45 µs
MedleyRouter            3.89 µs/iter     (3.74 µs … 4.41 µs)   3.93 µs   4.41 µs   4.41 µs
FindMyWay              54.18 µs/iter   (40.46 µs … 16.06 ms)  54.29 µs  81.92 µs  91.33 µs
KoaTreeRouter           3.35 µs/iter     (3.08 µs … 4.97 µs)   3.28 µs   4.97 µs   4.97 µs
TrekRouter              4.87 µs/iter     (4.73 µs … 5.08 µs)   4.91 µs   5.08 µs   5.08 µs

summary for GET /user
  LinearRouter
   1.17x faster than PreparedRegExpRouter
   2.92x faster than KoaTreeRouter
   3.4x faster than MedleyRouter
   4.25x faster than TrekRouter
   5.08x faster than TrieRouter
   26.62x faster than RegExpRouter
   47.33x faster than FindMyWay

• GET /user/comments
------------------------------------------------------------ -----------------------------
RegExpRouter           31.15 µs/iter  (25.58 µs … 896.29 µs)  32.42 µs  46.75 µs  54.83 µs
PreparedRegExpRouter    1.38 µs/iter  (575.25 ns … 30.94 µs) 783.92 ns  30.94 µs  30.94 µs
TrieRouter              6.15 µs/iter     (5.38 µs … 8.52 µs)   6.45 µs   8.52 µs   8.52 µs
LinearRouter            1.26 µs/iter     (1.17 µs … 1.89 µs)   1.28 µs   1.89 µs   1.89 µs
MedleyRouter            4.07 µs/iter     (3.89 µs … 4.84 µs)   4.08 µs   4.84 µs   4.84 µs
FindMyWay              52.99 µs/iter    (40.88 µs … 8.88 ms)  53.75 µs  81.25 µs  87.21 µs
KoaTreeRouter           3.79 µs/iter     (3.16 µs … 9.26 µs)   3.71 µs   9.26 µs   9.26 µs
TrekRouter              5.15 µs/iter     (4.92 µs … 5.76 µs)   5.15 µs   5.76 µs   5.76 µs

summary for GET /user/comments
  LinearRouter
   1.1x faster than PreparedRegExpRouter
   3.01x faster than KoaTreeRouter
   3.23x faster than MedleyRouter
   4.09x faster than TrekRouter
   4.88x faster than TrieRouter
   24.72x faster than RegExpRouter
   42.06x faster than FindMyWay

• GET /user/lookup/username/hey
------------------------------------------------------------ -----------------------------
RegExpRouter           30.84 µs/iter    (24.17 µs … 5.83 ms)  32.17 µs   45.5 µs  54.21 µs
PreparedRegExpRouter    1.18 µs/iter  (604.05 ns … 16.35 µs) 906.84 ns  16.35 µs  16.35 µs
TrieRouter             10.31 µs/iter    (5.73 µs … 72.14 µs)   6.87 µs  72.14 µs  72.14 µs
LinearRouter            1.47 µs/iter     (1.39 µs … 1.59 µs)   1.49 µs   1.59 µs   1.59 µs
MedleyRouter            4.49 µs/iter     (4.19 µs … 5.79 µs)   4.52 µs   5.79 µs   5.79 µs
FindMyWay              54.68 µs/iter    (42.92 µs … 9.42 ms)  55.46 µs     81 µs  85.33 µs
KoaTreeRouter           3.56 µs/iter      (3.2 µs … 6.45 µs)   3.49 µs   6.45 µs   6.45 µs
TrekRouter              5.39 µs/iter     (5.05 µs … 9.84 µs)    5.2 µs   9.84 µs   9.84 µs

summary for GET /user/lookup/username/hey
  PreparedRegExpRouter
   1.24x faster than LinearRouter
   3.01x faster than KoaTreeRouter
   3.79x faster than MedleyRouter
   4.56x faster than TrekRouter
   8.7x faster than TrieRouter
   26.05x faster than RegExpRouter
   46.18x faster than FindMyWay

• GET /event/abcd1234/comments
------------------------------------------------------------ -----------------------------
RegExpRouter           34.33 µs/iter    (25.54 µs … 6.94 ms)  33.04 µs  90.96 µs 150.29 µs
PreparedRegExpRouter    1.09 µs/iter    (597.1 ns … 6.46 µs)   1.02 µs   6.46 µs   6.46 µs
TrieRouter              9.84 µs/iter   (4.67 µs … 121.14 ms)   7.13 µs  14.17 µs  19.54 µs
LinearRouter            1.56 µs/iter     (1.36 µs … 1.82 µs)   1.62 µs   1.82 µs   1.82 µs
MedleyRouter            4.49 µs/iter     (4.09 µs … 4.76 µs)   4.63 µs   4.76 µs   4.76 µs
FindMyWay              56.66 µs/iter    (42.75 µs … 1.88 ms)  56.79 µs 100.67 µs 121.04 µs
KoaTreeRouter           3.61 µs/iter     (3.21 µs … 7.28 µs)   3.53 µs   7.28 µs   7.28 µs
TrekRouter              5.43 µs/iter     (4.97 µs … 6.57 µs)   5.71 µs   6.57 µs   6.57 µs

summary for GET /event/abcd1234/comments
  PreparedRegExpRouter
   1.44x faster than LinearRouter
   3.32x faster than KoaTreeRouter
   4.13x faster than MedleyRouter
   5x faster than TrekRouter
   9.06x faster than TrieRouter
   31.61x faster than RegExpRouter
   52.17x faster than FindMyWay

• POST /event/abcd1234/comment
------------------------------------------------------------ -----------------------------
RegExpRouter           32.84 µs/iter  (25.67 µs … 931.71 µs)  35.04 µs  50.38 µs  59.13 µs
PreparedRegExpRouter    1.86 µs/iter  (703.25 ns … 19.42 µs)   1.22 µs  19.42 µs  19.42 µs
TrieRouter             15.42 µs/iter   (6.05 µs … 142.22 µs)   9.01 µs 142.22 µs 142.22 µs
LinearRouter          573.28 ns/iter   (468.48 ns … 1.16 µs) 562.93 ns   1.16 µs   1.16 µs
MedleyRouter            4.57 µs/iter      (4.01 µs … 5.6 µs)    4.7 µs    5.6 µs    5.6 µs
FindMyWay              60.21 µs/iter    (41.63 µs … 7.77 ms)  55.67 µs 105.25 µs 147.08 µs
KoaTreeRouter           3.73 µs/iter     (3.32 µs … 7.55 µs)   3.68 µs   7.55 µs   7.55 µs
TrekRouter              5.77 µs/iter     (5.37 µs … 6.23 µs)   6.01 µs   6.23 µs   6.23 µs

summary for POST /event/abcd1234/comment
  LinearRouter
   3.25x faster than PreparedRegExpRouter
   6.5x faster than KoaTreeRouter
   7.97x faster than MedleyRouter
   10.07x faster than TrekRouter
   26.9x faster than TrieRouter
   57.28x faster than RegExpRouter
   105.02x faster than FindMyWay

• GET /very/deeply/nested/route/hello/there
------------------------------------------------------------ -----------------------------
RegExpRouter           38.99 µs/iter     (26.88 µs … 1.5 ms)  41.25 µs  99.33 µs 115.13 µs
PreparedRegExpRouter  987.13 ns/iter   (618.27 ns … 2.18 µs)   1.05 µs   2.18 µs   2.18 µs
TrieRouter              8.33 µs/iter    (6.15 µs … 13.69 µs)  10.29 µs  13.69 µs  13.69 µs
LinearRouter            1.86 µs/iter     (1.38 µs … 3.83 µs)   2.06 µs   3.83 µs   3.83 µs
MedleyRouter             4.9 µs/iter     (4.29 µs … 6.77 µs)   5.09 µs   6.77 µs   6.77 µs
FindMyWay              73.58 µs/iter    (45.88 µs … 2.08 ms)  71.46 µs 252.63 µs    405 µs
KoaTreeRouter           4.12 µs/iter     (3.24 µs … 7.15 µs)   4.46 µs   7.15 µs   7.15 µs
TrekRouter              5.96 µs/iter     (5.07 µs … 7.89 µs)   6.45 µs   7.89 µs   7.89 µs

summary for GET /very/deeply/nested/route/hello/there
  PreparedRegExpRouter
   1.88x faster than LinearRouter
   4.18x faster than KoaTreeRouter
   4.96x faster than MedleyRouter
   6.04x faster than TrekRouter
   8.44x faster than TrieRouter
   39.5x faster than RegExpRouter
   74.54x faster than FindMyWay

• GET /static/index.html
------------------------------------------------------------ -----------------------------
RegExpRouter            38.2 µs/iter     (26.29 µs … 1.5 ms)  40.21 µs  83.33 µs 121.96 µs
PreparedRegExpRouter    1.33 µs/iter   (894.09 ns … 2.23 µs)   1.66 µs   2.23 µs   2.23 µs
TrieRouter             14.67 µs/iter     (4.67 µs … 2.86 ms)  12.67 µs 117.17 µs 197.13 µs
LinearRouter            2.04 µs/iter     (1.34 µs … 5.22 µs)   2.37 µs   5.22 µs   5.22 µs
MedleyRouter            6.41 µs/iter    (4.99 µs … 12.08 µs)   6.65 µs  12.08 µs  12.08 µs
FindMyWay              72.48 µs/iter    (41.42 µs … 1.62 ms)  68.83 µs 280.33 µs 408.92 µs
KoaTreeRouter           4.76 µs/iter      (3.4 µs … 7.76 µs)   5.53 µs   7.76 µs   7.76 µs
TrekRouter              6.68 µs/iter     (5.18 µs … 9.86 µs)   8.48 µs   9.86 µs   9.86 µs

summary for GET /static/index.html
  PreparedRegExpRouter
   1.53x faster than LinearRouter
   3.57x faster than KoaTreeRouter
   4.8x faster than MedleyRouter
   5.01x faster than TrekRouter
   11x faster than TrieRouter
   28.65x faster than RegExpRouter
   54.36x faster than FindMyWay

Bundlesize

I compared the app created by npm create sonik@latest with the following changes.

Add RegExpRouter preset

import { HonoBase } from './hono-base'
import type { HonoOptions } from './hono-base'
import { RegExpRouter } from './router/reg-exp-router'
import type { Env, Schema } from './types'

export class Hono<
  E extends Env = Env,
  S extends Schema = {},
  BasePath extends string = '/'
> extends HonoBase<E, S, BasePath> {
  constructor(options: HonoOptions<E> = {}) {
    super(options)
    this.router = new RegExpRouter()
  }
}

Add rollup plugin

import { defineConfig } from "vite";
import sonik from "sonik/vite";
import pages from "@sonikjs/cloudflare-pages";

import {
  buildInitParams,
  serializeInitParams,
} from "../../honojs/hono/src/router/reg-exp-router";

// replace RegExpRouter with PreparedRegExpRouter at build time
function replacePreparedRegExpRouter(initPrams) {
  return {
    name: "hono-prepared-reg-exp-router",
    load(id) {
      const match = id.match(/router\/reg-exp-router\/index.(js|ts)$/);
      if (match) {
        const ext = match[1];
        const serialized = serializeInitParams(buildInitParams(initPrams));
        return `
import { PreparedRegExpRouter } from './prepared-router.${ext}'
export class RegExpRouter extends PreparedRegExpRouter {
  constructor() {
    super(...${serialized});
  }
}
`;
      }
      return null;
    },
  };
}

export default defineConfig({
  plugins: [
    replacePreparedRegExpRouter({
      routes: [
        {
          method: "ALL",
          path: "/about/*",
        },
        {
          method: "ALL",
          path: "/*",
        },
        {
          method: "ALL",
          path: "/static/*",
        },
        {
          method: "GET",
          path: "/about/:name",
        },
        {
          method: "GET",
          path: "/",
        },
      ],
    }),
    sonik(),
    pages(),
  ],
});

With this setup, the npx vite build resulted in 71.27 kB -> 55.30 kB.

When can we use it?

It can be used for general applications, but it is a bit difficult to use.
File-based routing has the following characteristics that make it easy to implement.

  • Routing that falls back to TrieRouter is not generated; can assume RegExpRouter.
  • Basically, we can know the routing in advance because it is done via a build tool.

Author should do the followings, if applicable

  • Add tests
  • Run tests
  • yarn denoify to generate files for Deno

@usualoma
Copy link
Member Author

usualoma commented Dec 9, 2023

Hi, @yusukebe.
What do you think about this?

@yusukebe
Copy link
Member

Hi @usualoma

Interesting approach. I'm glad you are interested in file-based routing!

Routing that falls back to TrieRouter is not generated; can assume RegExpRouter.
Basically, we can know the routing in advance because it is done via a build tool.

Exactly right. We could make Hono specialize in file-based routing and improve the router. However, it might be too much optimization. I'd like to take some more time to think about it.

This might not be related, but it's one idea. Can we improve performance like this with explicitly build routes?

app.get('/', (c) => c.json(0))
app.get('/a', (c) => c.json(0))
app.get('/b', (c) => c.json(0))

export default app.build()

@usualoma
Copy link
Member Author

@yusukebe Thank you for your comment.

After creating the PR, I thought about it for a while. File-based routing can easily extract paths, but it is expensive to run import to extract even the method names (GET, POST, etc) used.
Based on this, I changed the API in 54b82ca to "just list the paths that may be used". If we proceed with this PR, I would like to do it this way.

before

    buildInitParams({
      routes: [
        { method: 'ALL', path: '*' },
        { method: 'ALL', path: '/posts/:id/*' },
        { method: 'GET', path: '*' },
        { method: 'GET', path: '/' },
        { method: 'GET', path: '/static' },
        { method: 'GET', path: '/posts/:id/*' },
        { method: 'GET', path: '/posts/:id' },
        { method: 'GET', path: '/posts/:id/comments' },
        { method: 'POST', path: '/posts' },
        { method: 'PUT', path: '/posts/:id' },
      ],
    })

after

    buildInitParams({
      paths: ['*', '/static', '/posts/:id/*', '/posts/:id', '/posts/:id/comments', '/posts'],
    })

@usualoma
Copy link
Member Author

@yusukebe

This might not be related, but it's one idea. Can we improve performance like this with explicitly build routes?

I guess you mean "finish initializing the router before the first request comes in". If so, I think the following changes will do it.

diff --git a/src/hono-base.ts b/src/hono-base.ts
index f118a741..b4539b74 100644
--- a/src/hono-base.ts
+++ b/src/hono-base.ts
@@ -236,10 +236,14 @@ class Hono<
   }
 
   get routerName() {
-    this.matchRoute('GET', '/')
+    this.build()
     return this.router.name
   }
 
+  build() {
+    this.router.build?.()
+    return this
+  }
+
   /**
    * @deprecated
    * `app.head()` is no longer used.
diff --git a/src/router.ts b/src/router.ts
index 78cd65e5..71e45005 100644
--- a/src/router.ts
+++ b/src/router.ts
@@ -8,6 +8,7 @@ export interface Router<T> {
   name: string
   add(method: string, path: string, handler: T): void
   match(method: string, path: string): Result<T>
+  build?(): void
 }
 
 export type ParamIndexMap = Record<string, number>
diff --git a/src/router/reg-exp-router/router.ts b/src/router/reg-exp-router/router.ts
index 82a8d0ea..30affc24 100644
--- a/src/router/reg-exp-router/router.ts
+++ b/src/router/reg-exp-router/router.ts
@@ -208,6 +208,11 @@ export class RegExpRouter<T> implements Router<T> {
   }
 
   match(method: string, path: string): Result<T> {
+    this.build()
+    return this.match(method, path)
+  }
+
+  build() {
     clearWildcardRegExpCache() // no longer used.
 
     const matchers = this.buildAllMatchers()
@@ -228,8 +233,6 @@ export class RegExpRouter<T> implements Router<T> {
       const index = match.indexOf('', 1)
       return [matcher[1][index], match]
     }
-
-    return this.match(method, path)
   }
 
   private buildAllMatchers(): Record<string, Matcher<T>> {

@usualoma
Copy link
Member Author

And maybe SmartRouter. I have not confirmed that it works.

diff --git a/src/router/smart-router/router.ts b/src/router/smart-router/router.ts
index 7b0510d8..3a970ca8 100644
--- a/src/router/smart-router/router.ts
+++ b/src/router/smart-router/router.ts
@@ -20,6 +20,11 @@ export class SmartRouter<T> implements Router<T> {
   }
 
   match(method: string, path: string): Result<T> {
+    this.build()
+    return this.match(method, path)
+  }
+
+  build() {
     if (!this.routes) {
       throw new Error('Fatal error')
     }
@@ -27,14 +32,13 @@ export class SmartRouter<T> implements Router<T> {
     const { routers, routes } = this
     const len = routers.length
     let i = 0
-    let res
     for (; i < len; i++) {
       const router = routers[i]
       try {
         routes.forEach((args) => {
           router.add(...args)
         })
-        res = router.match(method, path)
+        router.build?.()
       } catch (e) {
         if (e instanceof UnsupportedPathError) {
           continue
@@ -55,8 +59,6 @@ export class SmartRouter<T> implements Router<T> {
 
     // e.g. "SmartRouter + RegExpRouter"
     this.name = `SmartRouter + ${this.activeRouter.name}`
-
-    return res as Result<T>
   }
 
   get activeRouter() {

@yusukebe
Copy link
Member

yusukebe commented Dec 10, 2023

@usualoma

For example, in this project that provides file-based routing:

https://github.com/yusukebe/file-base-routing-framework

We can write src/framework.ts using PreparedRegExpRouter as follows:

diff --git a/src/framework.ts b/src/framework.ts
index d75e3b9..e781263 100644
--- a/src/framework.ts
+++ b/src/framework.ts
@@ -35,7 +35,19 @@ const ROUTES = import.meta.glob<RouteFile>('/app/routes/**/[a-z0-9[-][a-z0-9[_-]
 })

 export const createApp = () => {
-  const app = new Hono()
+  const paths: string[] = []
+  Object.keys(ROUTES).map((key) => {
+    paths.push(filePathToPath(key.replace(/^\/app\/routes/, '')))
+  })
+
+  const preparedParams = buildInitParams({
+    paths
+  })
+
+  const app = new HonoBase({
+    // @ts-ignore
+    router: new PreparedRegExpRouter(preparedParams[0], preparedParams[1])
+  })

   for (const [key, routes] of Object.entries(RENDERERS)) {
     const dirPath = pathToDirPath(key)

The bundle size difference:

  • Default: dist/_worker.js 30.39 kB
  • Only PreparedRegExpRouter: dist/_worker.js 27.84 kB

The difference isn't as significant as I expected, but this approach allows us to implement it in the framework without creating Vite or Rollup plugins.

@yusukebe
Copy link
Member

yusukebe commented Dec 10, 2023

I guess you mean "finish initializing the router before the first request comes in".

Yes, I mean that. But, I think we can make it like the following:

diff --git a/src/hono-base.ts b/src/hono-base.ts
index f118a74..40a3ec4 100644
--- a/src/hono-base.ts
+++ b/src/hono-base.ts
@@ -5,6 +5,7 @@ import { HTTPException } from './http-exception'
 import { HonoRequest } from './request'
 import type { Router } from './router'
 import { METHOD_NAME_ALL, METHOD_NAME_ALL_LOWERCASE, METHODS } from './router'
+import { PreparedRegExpRouter, buildInitParams } from './router/reg-exp-router'
 import type {
   Env,
   ErrorHandler,
@@ -251,10 +252,7 @@ class Hono<
   }
 
   private addRoute(method: string, path: string, handler: H) {
-    method = method.toUpperCase()
-    path = mergePath(this.#basePath, path)
     const r: RouterRoute = { path: path, method: method, handler: handler }
-    this.router.add(method, path, [handler, r])
     this.routes.push(r)
   }
 
@@ -373,6 +371,24 @@ class Hono<
       event.respondWith(this.dispatch(event.request, event, undefined, event.request.method))
     })
   }
+
+  build = () => {
+    const paths = this.routes.map((route) => {
+      return route.path
+    })
+    console.log(paths)
+    const preparedParams = buildInitParams({
+      paths,
+    })
+    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+    // @ts-ignore
+    this.router = new PreparedRegExpRouter(preparedParams[0], preparedParams[1])
+    this.routes.map((route) => {
+      const r: RouterRoute = { path: route.path, method: route.method, handler: route.handler }
+      this.router.add(route.method.toUpperCase(), route.path, [route.handler, r])
+    })
+    return this
+  }
 }
 
 export { Hono as HonoBase }

The bundle size difference:

yusuke $ wrangler deploy --minify --dry-run src/default-router.ts
 ⛅️ wrangler 3.9.0 (update available 3.19.0)
------------------------------------------------------
Total Upload: 21.95 KiB / gzip: 8.04 KiB
--dry-run: exiting now.

yusuke $ wrangler deploy --minify --dry-run src/prepared-router.ts
 ⛅️ wrangler 3.9.0 (update available 3.19.0)
------------------------------------------------------
Total Upload: 18.27 KiB / gzip: 7.09 KiB
--dry-run: exiting now.

Nevertheless, this code actually works, but it is not practical to write this process in hono-base.ts.

@usualoma
Copy link
Member Author

@yusukebe Thank you so much!
I think the following result is "smaller because TrieRouter is no longer included".

Default: dist/_worker.js 30.39 kB
Only PreparedRegExpRouter: dist/_worker.js 27.84 kB

The main point of PreparedRegExpRouter is to exclude the following source code from the bundle

src/router/reg-exp-router/{trie,node}.ts

Therefore, we need to somehow work with the bundling tool to remove the following code.

import { RegExpRouter } from '. /router/reg-exp-router'

@yusukebe
Copy link
Member

yusukebe commented Dec 10, 2023

Therefore, we need to somehow work with the bundling tool to remove the following code.

Ah, I didn't write that, but it imports them like this. So, it does not import RegExpRouter or TrieRouter:

import { HonoBase } from '/Users/yusuke/work/honojs/hono/src/hono-base'
import {
  PreparedRegExpRouter,
  buildInitParams
} from '/Users/yusuke/work/honojs/hono/src/router/reg-exp-router/prepared-router'
import type { H, MiddlewareHandler } from '/Users/yusuke/work/honojs/hono/src/types'

The entire diff:

diff --git a/src/framework.ts b/src/framework.ts
index d75e3b9..96d4316 100644
--- a/src/framework.ts
+++ b/src/framework.ts
@@ -1,5 +1,9 @@
-import { Hono } from 'hono'
-import type { H, MiddlewareHandler } from 'hono/types'
+import { HonoBase } from '/Users/yusuke/work/honojs/hono/src/hono-base'
+import {
+  PreparedRegExpRouter,
+  buildInitParams
+} from '/Users/yusuke/work/honojs/hono/src/router/reg-exp-router/prepared-router'
+import type { H, MiddlewareHandler } from '/Users/yusuke/work/honojs/hono/src/types'

 const METHODS = ['GET', 'POST', 'PUT', 'DELETE'] as const

@@ -35,7 +39,19 @@ const ROUTES = import.meta.glob<RouteFile>('/app/routes/**/[a-z0-9[-][a-z0-9[_-]
 })

 export const createApp = () => {
-  const app = new Hono()
+  const paths: string[] = []
+  Object.keys(ROUTES).map((key) => {
+    paths.push(filePathToPath(key.replace(/^\/app\/routes/, '')))
+  })
+
+  const preparedParams = buildInitParams({
+    paths
+  })
+
+  const app = new HonoBase({
+    // @ts-ignore
+    router: new PreparedRegExpRouter(preparedParams[0], preparedParams[1])
+  })

   for (const [key, routes] of Object.entries(RENDERERS)) {
     const dirPath = pathToDirPath(key)

@usualoma
Copy link
Member Author

@yusukebe

Yeah, I know, but buildInitParams() depends on RegExpRouter(), so when we import buildInitParams(), it bundles the following source code.

src/router/reg-exp-router/{trie,node}.ts
https://github.com/honojs/hono/pull/1796/files#diff-05f98af8ac89c7dd686b79c41d8167478c790c0051245d79c3b0f0554351dc63R77

And buildInitParams() is as heavy as the normal RegExpRouter() initialization, so it must be done before bundling to optimize.

@usualoma
Copy link
Member Author

Ah, but I don't deny the following opinion.

it might be too much optimization

This code is very high-performing, but it might be too complicated.

We might be happier to optimize PatternRouter than this PR approach.

@yusukebe
Copy link
Member

Yeah, I know, but buildInitParams() depends on RegExpRouter(), so when we import buildInitParams(), it bundles the following source code.

And buildInitParams() is as heavy as the normal RegExpRouter() initialization, so it must be done before bundling to optimize.

I see, you are right. But, I don't want to create this router and plugins for bundlers just for this optimization. Perhaps we could enhance performance further with static analytics before bundling, but that would be really too much optimization.

Either way, this PR has inspired us. Let's keep it open still.

@yusukebe
Copy link
Member

In addition, It is a good idea to have a "RegExpRouter" preset for file-based routing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants