Hono 工程化实战 03:性能优化从哪里下手
wxk1991 Lv5

Hono 工程化实战 03:性能优化从哪里下手

Hono 很快。

但这不代表你的 Hono 应用一定快。

框架快,只说明框架本身的开销小。

真正的接口耗时通常花在这里:

1
2
3
4
5
6
7
8
9
数据库查询
第三方 API
JSON 序列化
权限查询
对象存储
缓存 miss
冷启动
过重 middleware
不合理的分页

所以 Hono 项目做性能优化,不能只盯着框架 benchmark。

要从真实请求链路下手。

这篇文章讲 Hono API 性能优化的实际顺序:先测量,再减少无效工作,再缓存,再优化数据库,最后才考虑运行时和部署形态。

技术栈快照时间:2026-06-23。Hono middleware、compress、timing、各运行时适配器会更新,具体行为以官方文档为准。


一、先测量,不要先猜

性能优化最常见的错误是凭感觉。

比如:

1
2
3
4
我觉得是 Hono 慢
我觉得是 JSON 慢
我觉得是 Cloudflare 慢
我觉得是数据库慢

这都没用。

先加基础耗时日志:

1
2
3
4
5
6
7
8
9
10
11
12
app.use('*', async (c, next) => {
const start = Date.now()

await next()

console.info('request_timing', {
method: c.req.method,
path: new URL(c.req.url).pathname,
status: c.res.status,
durationMs: Date.now() - start,
})
})

然后对慢操作分段:

1
2
3
const user = await measure('db.user.find', () => users.findById(userId))
const posts = await measure('db.posts.list', () => postsRepo.list(user.id))
const profile = await measure('third_party.profile', () => fetchProfile(user.id))

没有测量就优化,基本是在碰运气。


二、先减少不必要的 middleware

Hono middleware 很好用。

但不要把所有 middleware 都挂到 *

错误示例:

1
2
3
4
app.use('*', auth())
app.use('*', permission())
app.use('*', heavyAuditLog())
app.use('*', parseTenant())

这样连 /health/public/* 都要跑一堆逻辑。

更合理:

1
2
3
4
app.use('/api/*', requestId())
app.use('/api/*', requestLogger())
app.use('/api/private/*', auth())
app.use('/api/admin/*', requireAdmin())

公共接口不要跑私有接口的鉴权。

健康检查不要查数据库。

静态资源不要跑业务 middleware。

性能优化第一步不是写缓存。

是少做没必要的事。


三、middleware 顺序也影响性能

推荐顺序:

1
2
3
4
5
6
7
8
9
10
requestId
安全 header
CORS
body limit
请求日志开始
轻量鉴权
租户解析
权限检查
业务 handler
请求日志结束

为什么 bodyLimit 要早?

因为大请求应该尽早拒绝。

为什么权限检查在业务 handler 前?

因为没权限就不要查业务数据。

为什么 requestId 要最早?

因为后面的日志都需要它。

顺序不是洁癖。

顺序会影响失败请求的成本。


四、压缩不要重复做

Hono 有 compress middleware:

1
2
3
import { compress } from 'hono/compress'

app.use('*', compress())

但不是所有运行时都需要你手动压缩。

Hono 文档提醒,在 Cloudflare Workers、Deno Deploy 这类平台上,响应体可能已经由平台处理压缩。

所以要先确认部署环境。

如果你在 Node.js 自建服务上,compress middleware 可能有价值。

如果你在边缘平台上,重复压缩可能没有意义,甚至增加开销。

性能优化里有一条很重要:

1
不要优化已经由平台处理的问题

五、缓存优先缓存读多写少

不是所有接口都适合缓存。

适合缓存:

1
2
3
4
5
6
7
公开文章列表
公开文章详情
配置项
字典数据
产品价格
用户权限快照
首页聚合数据

不适合缓存:

1
2
3
4
5
支付创建
订单状态变更
实时库存扣减
强一致后台操作
敏感用户数据

缓存前先问三个问题:

1
2
3
数据多久变化一次?
允许用户看到旧数据多久?
数据是否和用户身份强相关?

如果答不上来,就先别缓存。


六、HTTP 缓存是最便宜的优化

公开 GET 接口可以先从 HTTP header 开始。

例如公开文章详情:

1
2
3
4
5
6
7
8
9
10
11
app.get('/api/public/posts/:slug', async (c) => {
const post = await getPublishedPost(c.req.param('slug'))

if (!post) {
return c.json({ error: { code: 'POST_NOT_FOUND' } }, 404)
}

c.header('Cache-Control', 'public, max-age=60, stale-while-revalidate=300')

return c.json({ data: post })
})

这类缓存不用引入新组件。

浏览器、CDN、边缘缓存都能理解。

如果内容变化不频繁,再加 ETag:

1
2
3
4
5
6
7
const etag = `"post-${post.id}-${post.updatedAt}"`

if (c.req.header('If-None-Match') === etag) {
return new Response(null, { status: 304 })
}

c.header('ETag', etag)

很多接口根本不需要复杂缓存系统。

HTTP 缓存已经够用。


七、KV 缓存适合昂贵聚合

如果某个接口要查多张表、做聚合、再调第三方,可以用 KV 缓存结果。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
app.get('/api/dashboard', async (c) => {
const tenant = c.get('tenant')
const cacheKey = `tenant:${tenant.id}:dashboard:v1`

const cached = await c.env.CACHE_KV.get(cacheKey, 'json')

if (cached) {
return c.json({
data: cached,
meta: { cache: 'hit' },
})
}

const data = await buildDashboard(tenant.id, c.env.DB)

await c.env.CACHE_KV.put(cacheKey, JSON.stringify(data), {
expirationTtl: 60,
})

return c.json({
data,
meta: { cache: 'miss' },
})
})

注意 key 要带版本:

1
tenant:<tenantId>:dashboard:v1

以后结构变化,可以升级到 v2,不用担心旧缓存污染新代码。


八、缓存失效比缓存读取更难

缓存写进去很容易。

难的是它什么时候失效。

常见策略:

1
2
3
4
5
短 TTL
写操作后删除缓存
版本化 key
后台预热
stale-while-revalidate

早期项目我更推荐:

1
短 TTL + 写操作删除缓存

例如更新文章后:

1
2
3
await updatePost(postId, input)
await c.env.CACHE_KV.delete(`post:${postId}:v1`)
await c.env.CACHE_KV.delete(`tenant:${tenant.id}:posts:list:v1`)

不要一开始就设计复杂缓存一致性。

先让缓存最多旧几十秒。

这对大多数内容型 API 已经够用。


九、数据库优化通常更重要

如果接口慢在数据库,Hono 再快也救不了。

常见问题:

1
2
3
4
5
6
没有索引
分页 offset 太深
N+1 查询
一次返回太多字段
模糊搜索扫全表
多租户查询没组合索引

多租户文章列表应该有类似索引:

1
2
create index idx_posts_tenant_created
on posts (tenant_id, created_at desc);

查询只拿需要字段:

1
2
3
4
5
select id, title, status, created_at
from posts
where tenant_id = ?
order by created_at desc
limit 20

不要接口只需要标题,却把 content 大字段也查出来。

很多性能问题就是这样一点点堆出来的。


十、分页不要无限 offset

普通后台列表用 offset 没问题。

但数据量大时,深分页会越来越慢:

1
2
3
4
select *
from posts
order by created_at desc
limit 20 offset 200000;

更适合 API 的方式是游标分页:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
app.get('/api/posts', async (c) => {
const cursor = c.req.query('cursor')

const posts = await listPosts({
cursor,
limit: 20,
})

return c.json({
data: posts.items,
pageInfo: {
nextCursor: posts.nextCursor,
},
})
})

游标可以是:

1
2
3
created_at + id
自增 id
编码后的排序字段组合

对移动端、无限滚动、时间线列表,游标分页更稳。


十一、运行时选择不是第一优化项

Hono 可以跑在很多运行时:

1
2
3
4
5
6
7
Cloudflare Workers
Node.js
Bun
Deno
Vercel
Netlify
AWS Lambda

这很酷。

但性能问题不要一上来就怪运行时。

先确认:

1
2
3
4
5
6
7
慢请求耗时分布
数据库位置
缓存命中率
第三方 API 耗时
冷启动比例
区域延迟
序列化成本

如果用户都在中国大陆,而数据库在美国,换框架意义不大。

如果每个请求都查 20 次数据库,换 Bun 也解决不了根因。

运行时选择要服务于架构,不是替代架构。


十二、性能优化顺序

我推荐这个顺序:

1
2
3
4
5
6
7
8
9
1. 加请求耗时日志
2. 找出 P95/P99 慢接口
3. 分段测量数据库、缓存、第三方调用
4. 减少不必要 middleware
5. 修索引和查询字段
6. 给公开 GET 加 HTTP 缓存
7. 给昂贵聚合加 KV/内存缓存
8. 把慢操作丢到队列
9. 再评估运行时和部署区域

不要反过来。

一上来就换平台、换数据库、换框架,很容易忙半天只优化了 5%。

Hono 的性能优势是它足够轻,不会成为主要瓶颈。

这反而要求你把注意力放回真正的系统瓶颈上。

快,不是 benchmark 上的数字。

快,是用户请求从进入系统到拿到结果的整个链路都没有浪费。