惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

GbyAI
GbyAI
爱范儿
爱范儿
Y
Y Combinator Blog
T
Tor Project blog
V
Visual Studio Blog
U
Unit 42
B
Blog RSS Feed
博客园 - 叶小钗
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
阮一峰的网络日志
阮一峰的网络日志
T
Tailwind CSS Blog
G
Google Developers Blog
I
InfoQ
Stack Overflow Blog
Stack Overflow Blog
IT之家
IT之家
Microsoft Azure Blog
Microsoft Azure Blog
T
The Blog of Author Tim Ferriss
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
The Cloudflare Blog
Google DeepMind News
Google DeepMind News
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
H
Hackread – Cybersecurity News, Data Breaches, AI and More
F
Fortinet All Blogs
人人都是产品经理
人人都是产品经理
Apple Machine Learning Research
Apple Machine Learning Research
The GitHub Blog
The GitHub Blog
Recorded Future
Recorded Future
博客园_首页
罗磊的独立博客
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
量子位
P
Proofpoint News Feed
Jina AI
Jina AI
博客园 - 【当耐特】
S
Security @ Cisco Blogs
I
Intezer
MyScale Blog
MyScale Blog
Simon Willison's Weblog
Simon Willison's Weblog
P
Privacy & Cybersecurity Law Blog
腾讯CDC
T
Tenable Blog
A
Arctic Wolf
T
Threat Research - Cisco Blogs
S
Securelist
Know Your Adversary
Know Your Adversary
Spread Privacy
Spread Privacy
C
Check Point Blog
NISL@THU
NISL@THU
Microsoft Security Blog
Microsoft Security Blog
V
Vulnerabilities – Threatpost

Shadow Walker 松烟阁

AI Hasn’t Rebuilt the Organization Yet. Anxiety Rebuilt Management First AI 还没重构组织,焦虑先重构了管理 为什么偏偏是“左耳进,右耳出” 拆解 kimi-cli:Coding Agent 的能力上限,为什么在“模型之外”? 终端属于 Agent,但人类需要“仪表盘” A Deep Dive of LangGraph Mechanisms & Agent Design Patterns LangGraph 机制深度解析与Agent模式设计 The Physics of Inference – A Deep Dive into KV and Prompt Caching 从KV Cache到Prompt Cache的应用 Before Making AI Agent Systems Smarter, First Make Them Trustworthy 让Agent系统更聪明之前,先让它能被信任 在乌龟追上来之前 我的笔触在高维空间独一无二 大厂生存实录:从老板的“屎”论,谈谈如何去除内容的“AI味” 大厂生存实录之为什么是我做 Cursor与Vibe Coding 日本游记 —— 大阪京都 AI改变我们的编程方式 AI的一些思考和总结
我为 Memos 做了一个图片渲染服务
edony · 2026-02-23 · via Shadow Walker 松烟阁

趁着春节休假我给自己的 memos 系统补了一块一直想做的能力:把一条 memo 分享成一张可传播的卡片图。这件事看起来像是“截图”,但真正做起来我才发现,它本质上不是一个图片处理问题,而是一个系统设计问题。

我做下来的体会可以总结成一句话:不是在后端“画”一张图,而是在后端养了一台可控的浏览器,让它在正确的时机按下快门。

为什么要单独做一个渲染服务

从实现需求的角度,最直接的方案当然是把图片生成逻辑塞进主后端里,但我最后没有这么做。原因不是“不能做”,而是职责会变得很混乱。memos 后端擅长的是memo数据、权限、API等管理,而“把一个复杂页面稳定渲染成图”这件事,天然更像浏览器运行时的问题:

  • 字体是否加载完成
  • 图片是否加载完成
  • 主题和语言是否正确
  • 阴影、圆角、背景渐变是否和前端一致
  • 在高分屏下导出后是否仍然清晰

这些问题,如果在后端重新实现一套模板引擎和排版逻辑,等于把前端再写一遍。维护成本会很快失控。所以我最后做了一个内部服务 memos-images-rendering,专门负责一件事:

  • 接受渲染请求
  • 打开 Memos Web 的分享页
  • 等页面准备完成
  • 截图并返回 PNG

主后端负责鉴权和发令,渲染服务负责执行。边界一下就清楚了。

重要的前提:需要特别说明的一点是我有一台闲置的阿里云99一年的ECS,可以作为memos image rendering服务的环境,如果单为这个服务特意准备VPS/ECS就有点“为了瓶醋包了顿饺子”的味道了。

我为什么选择“浏览器渲染”,而不是“模板渲染”

如果只是输出一张图,很多人第一反应会是 Canvas、SVG、甚至服务端模板拼图。前一版本的share memo as image就是html2canvas的实现,但是在不同的环境(如iOS、macOS、Linux等)图片显示就会不一致。方案都能做但我最终还是选了 Playwright,原因很现实:

  • 前端已经有现成的分享页面样式
  • 主题(light/dark)和 locale 参数已经在 Web 层存在
  • 富文本、图片、emoji、CJK 字体渲染都交给浏览器
  • 样式改动后,渲染结果天然跟着前端走

也就是说,我复用的是“最终呈现结果”,而不是去复用一堆中间数据结构。这在工程上很重要。因为用户看到的从来不是 DTO,也不是 Markdown AST,用户看到的是最后那张图。

真正的难点不是截图,而是“什么时候截图”

做这种服务最容易低估的一点是:截图动作很简单,截图时机很难。如果你太早截图,常见问题会立刻出现:

  • 字体还没加载完成,文本换行错位
  • 图片还在加载,卡片出现空白块
  • 阴影和渐变还没稳定,导出的图看起来像半成品

很多人会用 networkidle,但线上页面经常有长连接、埋点、异步请求,这个信号并不可靠。所以我在这个服务里用的是一个更“业务化”的约定:

  • 分享页加载完成后,由前端显式设置 window.__MEMO_SHARE_READY__ = true
  • 渲染服务等待这个全局标记,再截图

这个设计有点像前后端之间的握手协议。 浏览器知道“DOM 出来了”,但只有业务页面自己知道“这张卡现在可以拍了”。这也是我这次实现里最关键的一条经验:渲染服务不应该猜页面状态,而应该和页面约定状态。

0:00

/0:29

两种模式:在“稳定输出”和“视觉效果”之间做了分层

为了兼顾不同分享场景,我做了两种渲染模式:

1. fixed 模式:要尺寸确定,就给你尺寸确定

这个模式会截图整个分享画布,然后把结果缩放到目标宽高(例如 2400x1350)。适合场景:

  • 社交平台封面图
  • 需要固定比例(例如 16:9
  • 需要稳定落地到某个模板位

优点是稳,结果尺寸完全可控。

2. auto 模式:围绕卡片智能裁切,保留一点“呼吸感”

这个模式会围绕 .share-memo-card 自动裁切,而不是死板地截满画布。我做了几个细节处理:

  • 给卡片周围保留背景 bleed
  • 给阴影额外留安全边距
  • 最终裁切区域会和 .share-memo-canvas 相交,防止越界

这样导出的图不会显得“贴边”,卡片视觉上更像一张真正的分享卡。当然,自动裁切一定会有失败边界(例如选择器缺失、布局异常)。 所以我给它做了兜底:一旦 auto crop 失败,自动回退到画布截图。

这背后的思路很简单:好看是加分项,稳定返回结果是底线。

性能优化不是先上分布式,而是先把单机跑顺

这种服务如果按“每次请求都启动浏览器”的方式写,基本很快就会卡住。所以我做了几层非常务实的优化:

  • 复用单个浏览器实例
  • 复用共享 browser context(缓存可复用)
  • 每次请求只新建 page,结束后关闭 page
  • 用队列限制并发(MAX_CONCURRENCY
  • 支持按空闲时间、渲染次数、存活时长回收浏览器

这几条加起来的效果,是把“浏览器启动成本”从每次请求里挪走,只在必要时付一次。另外还有一个经常被忽略的细节:清晰度。我这边默认用较高 DPR 渲染,再用 sharp 做高质量缩放。 这会让最终 PNG 在文字和细线条上更稳,不容易出现“能看,但发出去有点糊”的情况。换句话说,我不是只追求“生成成功”,而是追求“发出去像成品”。

可观测性:我希望看到它慢在哪里,而不是只知道它失败了

图片渲染服务很容易变成一个黑盒:请求进来,等几秒,成了或炸了。为了避免这个问题,我给它加了几类观测信息(可按环境开关):

  • 分阶段 timing 日志(gotowait ready、截图、裁切、resize
  • 请求级 request id
  • 可选响应头返回 timing 信息
  • 页面 console error / request failed 日志(用于排查前端资源问题)

这类服务一旦线上出问题,最怕的不是错误本身,而是“没有上下文”。 能看到每一阶段耗时,你才能判断问题在网络、页面、字体、图片,还是浏览器进程本身。

安全边界:这个服务只负责渲染,不负责做判断

这个服务是 internal-only 的,我没有把它设计成公开接口。完整链路里,鉴权责任在 memos 侧:

  • 后端生成短时效 share JWT
  • 渲染服务带着 token 打开分享页
  • 分享页/相关 API 完成验证
  • 渲染服务只拿最终页面做截图

也就是说,渲染服务不做业务权限判断,它只执行“拍照”动作。这是我这次实现里另一个很明确的选择:让权限留在权限系统里,让渲染留在渲染系统里。

这次实现让我重新确认的一件事

我以前会把这类需求归类为“媒体能力”或者“图片处理”,但这次做完后我更愿意把它叫作:“前端呈现的后端化执行”。它不是在服务端重新发明 UI,而是把浏览器变成一个受控运行时,把页面变成一个可验证的渲染契约。当你接受这个视角之后,很多设计决策都会变得自然:

  • 为什么要等 __MEMO_SHARE_READY__
  • 为什么要复用 browser/context
  • 为什么要做 autofixed 双模式
  • 为什么 auto crop 失败要回退
  • 为什么要把它做成内部服务,而不是暴露公网

这些不是“优化点”,而是这个系统能长期稳定运行的前提。

后续我还想做的事

目前版本已经能稳定支撑分享图生成,但还有一些值得继续打磨的方向:

  • 渲染结果缓存(相同 memo + theme + size 命中缓存)
  • 更细粒度的失败分类与重试策略
  • 可视化调试模式(导出裁切框信息)
  • 批量渲染/异步任务模式(适合预生成封面)
  • 更多模板(不是只有“截图卡片”,而是“设计化卡片”)

如果你也在做类似的“网页转图片”服务,我的建议是先别急着上复杂架构,先把这三件事做对:

  • 渲染时机要有业务握手
  • 失败路径要可回退
  • 浏览器生命周期要可控

剩下的扩展,都会容易很多。


总结这次实践,让我对Vibe Coding有了新的认识,对于一个Linux OS系统安全背景的人来讲,前端、浏览器这些永远在我的技能点之外的,但是Vibe Coding增强了我的技能树,所以不要把核心价值押注在补足模型能力缺口上,而是增强复杂系统的编排能力。

复杂系统的编排能力,包括数据孤岛、组织阻力、权限、习惯成本,这些是模型能力再强也吃不掉的,因为它们不是智能问题,是人和组织的问题。

但“编排能力”离不开专业领域知识,模型降低的是通用知识的门槛,但在医疗、法律、金融等领域,真正的专业判断力并不会被抹平。