Skip to content

Comments

feat: introduce optimize command#23

Merged
yusukebe merged 18 commits intomainfrom
feat/optimize
Oct 16, 2025
Merged

feat: introduce optimize command#23
yusukebe merged 18 commits intomainfrom
feat/optimize

Conversation

@usualoma
Copy link
Member

No description provided.

@usualoma usualoma changed the base branch from feat/serve to main October 11, 2025 22:41
@usualoma usualoma marked this pull request as ready for review October 12, 2025 06:42
@usualoma
Copy link
Member Author

@yusukebe
Would you please review this?

@usualoma usualoma requested a review from yusukebe October 12, 2025 06:43
@yusukebe
Copy link
Member

Hi @usualoma !

Thank you for the PR. Looks great! But we have something to discuss.

How can we use the generated file?

In the current implementation, hono optimize will generate hono-optimized.ts like this:

The source file src/index.ts:

import { Hono } from 'hono'

const app = new Hono()

app.get('/', (c) => c.text('Hi'))

export default app

The generated file:

// This file is generated by `hono optimize`
import { HonoBase } from 'hono/hono-base'
import type { HonoOptions } from 'hono/hono-base'
import { PreparedRegExpRouter } from 'hono/router/reg-exp-router'
import type { BlankEnv, BlankSchema, Env, Schema } from 'hono/types'

export class Hono<
  E extends Env = BlankEnv,
  S extends Schema = BlankSchema,
  BasePath extends string = '/',
> extends HonoBase<E, S, BasePath> {
  constructor(options: HonoOptions<E> = {}) {
    super(options)
    const routerParams = [
      { ALL: [/^$/, [], { '/': [[], []] }] },
      { '/': [[[''], null]] },
    ] as unknown as ConstructorParameters<typeof PreparedRegExpRouter>
    this.router = new PreparedRegExpRouter(...routerParams) as unknown as typeof this.router
  }
}

Then, how can I use this? Importing the Hono in the source file?

// src/index.ts
import { Hono } from './hono-optimized'

const app = new Hono()

app.get('/', (c) => c.text('Hi'))

export default app

Or, creating another file that imports the optimized Hono?

I know we can do it in many ways. However, the above cases are troublesome because we need to rewrite the import path to ./hono-optimized.

Another way to avoid changing the code is generating the optimized file, which includes optimized Hono and the "application" like this:

// src/hono-optimized-with-app.ts (the naming should be fixed)
import { HonoBase } from 'hono/hono-base'
import type { HonoOptions } from 'hono/hono-base'
import { PreparedRegExpRouter } from 'hono/router/reg-exp-router'
import type { BlankEnv, BlankSchema, Env, Schema } from 'hono/types'

class Hono<
  E extends Env = BlankEnv,
  S extends Schema = BlankSchema,
  BasePath extends string = '/',
> extends HonoBase<E, S, BasePath> {
  constructor(options: HonoOptions<E> = {}) {
    super(options)
    const routerParams = [
      { ALL: [/^$/, [], { '/': [[], []] }] },
      { '/': [[[''], null]] },
    ] as unknown as ConstructorParameters<typeof PreparedRegExpRouter>
    this.router = new PreparedRegExpRouter(...routerParams) as unknown as typeof this.router
  }
}

const app = new Hono()

app.get('/', (c) => c.text('Hi'))

export default app

If so, the user can run the optimized file directly:

$ bun run src/hono-optimized-with-app.ts

I don't know if that is a good way to include the app in the optimized Hono in the same file. But this is useful because the user can run it in one step after hono optimized.

The bundle file includes RegExpRouter

It's a problem that the generated file includes the RegExpRouter class. Try this:

$ hono optimize src/index.ts
$ esbuild --bundle src/hono-optimized.ts | grep RegExpRouter

The result is:

  var RegExpRouter = class {
    name = "RegExpRouter";
  var PreparedRegExpRouter = class {
    name = "PreparedRegExpRouter";
      this.router = new PreparedRegExpRouter(...routerParams);

I think the generated file doesn't have to include RegExpRouter to reduce the bundle size. I think we can avoid it by fixing the hono code.

@usualoma
Copy link
Member Author

Hi @yusukebe, thanks for your comment!

First, I will address the following points.

The bundle file includes RegExpRouter

The cause was on the hono side. This is expected to be fixed in the following PR.

honojs/hono#4458

@usualoma
Copy link
Member Author

How can we use the generated file?

I had envisioned a method where it would be rewritten as follows after generation.

// src/index.ts
import { Hono } from './hono-optimized'

const app = new Hono()

app.get('/', (c) => c.text('Hi'))

export default app

When hono optimize rewrites files, I think it would be difficult to handle configurations like the following...

// src/subapp.ts
import { Hono } from 'hono'

const app = new Hono()

app.get('/subapp', (c) => c.text('Hi'))

export default app
// src/index.ts
import { Hono } from 'hono'
import SubApp from './subapp'

const app = new Hono()

app.get('/', (c) => c.text('Hi'))
app.route('/', SubApp)

export default app

If we go as far as bundling with esbuild within hono optimize, I think we could replace the ‘hono’ module and bundle it.

@yusukebe
Copy link
Member

@usualoma

When hono optimize rewrites files, I think it would be difficult to handle configurations like the following...

I indeed... It's not a good idea to replace the application files.

If we go as far as bundling with esbuild within hono optimize, I think we could replace the ‘hono’ module and bundle it.

I think this "replace 'hono' module and bundle it" is a good solution. This is because of DX. Especially easy to use.

The word optimize sounds like "optimizing (only) the Hono and use it in your application". So, you may design it so. But as I said before, replacing the import path to './hono-optimized' is troublesome for users.

So I think these are better:

  • The command bundles the code and replaces hono.
  • The command doesn't minify, mangle, or otherwise optimize the code. Only bundle.
  • The file will be generated in the dist directory, and the user can change it with the --outputDir option.
  • The file my-app.ts will be renamed to be my-app-optimized.js. But I'm not sure this renaming is a good idea.

The use case will be:

  1. Create my app.
  2. Run hono optimized src/my-app.ts
  3. Run my favorite runtime for the optimized app: wrangler dev dist/my-app-optimized.js.

This is simple.

What do you think of it?

@yusukebe
Copy link
Member

Hey @usualoma

Released the latest version, including the change for tree shaking and bumped the hono in this repo: #29

@usualoma
Copy link
Member Author

@yusukebe
How about this?

assignRouterStatement = 'this.router = new TrieRouter()'
}

await esbuild.build({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about displaying a message like "Generated the optimized file into dist" using something like console.log?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about this?

CleanShot 2025-10-16 at 13 13 28@2x

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Displaying the router name and the file size is super cool!!

And it's a matter of taste, but shall we go without using emojis? I've never used it in this Hono CLI project. So how about the following:

CleanShot 2025-10-16 at 15 30 46@2x

Diff:

diff --git a/src/commands/optimize/index.ts b/src/commands/optimize/index.ts
index 7262cd4..8a8dd04 100644
--- a/src/commands/optimize/index.ts
+++ b/src/commands/optimize/index.ts
@@ -71,7 +71,8 @@ export function optimizeCommand(program: Command) {
         assignRouterStatement = 'this.router = new TrieRouter()'
       }

-      console.log(`⚡️Router: ${routerName}`)
+      console.log('[Optimized]')
+      console.log(`  Router: ${routerName}`)

       const outfile = resolve(process.cwd(), options.outfile)
       await esbuild.build({
@@ -127,6 +128,6 @@ export class Hono extends HonoBase {
       })

       const outfileStat = statSync(outfile)
-      console.log(`🔥App: ${options.outfile} (${(outfileStat.size / 1024).toFixed(2)} KB)`)
+      console.log(`  Output: ${options.outfile} (${(outfileStat.size / 1024).toFixed(2)} KB)`)
     })
 }

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the comment.
Okay, let's do that.
75596ab

@yusukebe
Copy link
Member

@usualoma

Cool! Almost are good! The bundle size became really small.

$ ls -la dist
-rw-r--r--@  1 yusuke  staff  10860 10月 16 10:32 index-not-opimized-tiny.js
-rw-r--r--@  1 yusuke  staff  18393 10月 16 10:31 index-not-opimized.js
-rw-r--r--@  1 yusuke  staff  11370 10月 16 10:30 index-optimized.js

I've added some comments.

Copy link
Member

@yusukebe yusukebe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@yusukebe
Copy link
Member

@usualoma

Thank you so much! Let's go!

@yusukebe yusukebe merged commit a0e00dc into main Oct 16, 2025
3 checks passed
@yusukebe yusukebe deleted the feat/optimize branch October 16, 2025 09:44
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.

2 participants