Posted in

去哪儿 Node 生成 1 亿张图片实践 (Satori + Sharp)_AI阅读总结 — 包阅AI

包阅导读总结

1. `Node`、`图片生成`、`Satori`、`Sharp`、`内存优化`

2. 本文介绍了 Java 后端和 Node 后端生成图片的方案,重点讲述了新的 Node 后端生成图片方式 Satori 及实践,包括其优点和实现过程,并针对 Satori + Resvg 方案的速度和内存问题进行优化。

3.

– Java 后端生成图片方案

– 利用 `java.awt` 的 `BufferedImage` 类,优点是跨多端,缺点是接口底层、绘图能力有限、易内存泄漏。

– Node 后端生成图片方案

– Headless Chromium 方案,优点是跨多端、绘图能力强,缺点是设置难、启动慢、资源消耗大。

– Satori 方案

– 将 HTML 和 CSS 转换成 SVG,优点是容易、快、轻量。

– 实践:基于 Satori + Resvg 实现 HTML 转 PNG,并封装为图片生成服务。

– 优化

– 速度优化:关闭内嵌字体优化,使用 Sharp 替换 Resvg-js。

– 内存优化:使用 jemalloc 内存管理器。

4.

思维导图:

文章地址:https://mp.weixin.qq.com/s/vp4v7e67rGbjNCPvjRD6Fw

文章来源:mp.weixin.qq.com

作者:任文龙

发布时间:2024/8/8 7:45

语言:中文

总字数:3879字

预计阅读时间:16分钟

评分:87分

标签:Node.js,图片生成,Satori,Sharp,性能优化


以下为原文内容

本内容来源于用户推荐转载,旨在分享知识与观点,如有侵权请联系删除 联系邮箱 media@ilingban.com

Java 后端 awt

我们之前的跨端方案是用 Java 后端方案。使用 Java java.awt 绘图库下的BufferedImage类来生成图片。这种生成图片的方式优点是一套方案跨多端,不挑前端环境。缺点是java.awt这类库接口比较底层,没有提供丰富的高级绘图能力,开发和维护布局复杂的图片对于平时较少接触界面开发的后端开发者来说非常有挑战。另一方面 Java 画图服务很容易发生内存泄露,给系统带来了很大的不稳定性因素。

下面是一张之前 Java 方案生成的车票图片:

Node 后端

和 Java 后端方案类似, 也可以使用前端同学更为熟悉的 Node 语言来实现图片生成。常见的方案是使用 Headless Chromium 来生成图片。Headless Chromium 是 Chromium 浏览器的无头版本,它可以在不打开浏览器的情况下运行 Chromium 浏览器。我们可以使用 Puppeteer 来控制 Headless Chromium 打开一个网页,然后将网页截图保存成图片。这种方式的优势是一套方案跨多端,不挑前端环境,绘图能力和浏览器一致,前端同学开发图片模板就和开发页面一样, 开发体验也要优于 java.awt 方案。不过现代浏览器带来丰富内置能力的同时也带来了臃肿的体积和较大的运行时内存占用。使用过此方案的 Vercel 总结了以下缺点:

  1. :该方案需要启动 Chromium,并使用 Puppeteer 对给定的 HTML 页面进行截图。设置这些工具很难实现,而且经常出错。

  2. :冷启动速度非常慢(平均约 4 秒),而且这可能会导致图片运行缓慢或损坏。

  3. :为了截图而启动整个浏览器并不高效,既昂贵又浪费计算资源。

  4. :Chromium 越来越大,Vercel 的 Serverless Function 里已经无法容纳了。

2、一种新的 Node 后端生成图片方式 Satori

Satori 是 Vercel 开源的一套将 HTML 和 CSS 转换成 SVG 的工具库。它支持 Node,浏览器,Web Worker 等环境。它的优点是:

  1. 容易: 不需要 Headless Chromium,它非常巧妙的把一种文本(HTML ,CSS)转换成另一种文本(SVG), SVG 可以方便的转换成各种图片。开发方式贴近前端页面开发,非常直观且容易上手。

  2. :整个转换过程非常巧妙的把一种基于文本的描述文档 HTML 转换成另一种基于文本的描述文档 SVG,实际没有真正绘制 HTML, 所以速度非常快。即便加上 SVG 转 PNG 的时间,也比 Headless Chromium 快 5 倍左右。

  3. 轻量:Satori 只有 3.9M ,加上图片转换工具也才 23.8M,而 Chromium 安装包就 200M,安装完 500M 起步。

三、实践

基于 Satori + Resvg 实现 HTML 转 PNG。

转换 HTML 到 SVG

参考 Satori 的文档,我们可以使用 Satori 将 HTML(JSX) 转换成 SVG。下面是一个简单的例子,将一个div转换成 SVG。

import fs from'fs';

import satori from'satori';

asyncfunctionhtml2svg() {

const fontData = fs.readFileSync('./assets/fonts/SourceHanSansSC-Normal.otf');

const svg = await satori(

<div

style={{

background:'#fff',

display:'flex',

width:'100%',

height:'100%',

alignItems:'center',

justifyContent:'center',

fontSize: 36,

color:'#000'

}}

>

Hello, World!

</div>,

{

width: 600,

height: 400,

fonts: [

{

name:'Source Han Sans SC',

data: fontData,

weight: 400,

style:'normal'

}

]

}

);

fs.writeFileSync('./hello.svg', svg);

}

html2svg();

Satori方法接收一个 jsxTree 和一个配置就能生成一张 SVG 图片。jsxTree 可以由一个类似 React 纯组件的函数封装返回,样式也可以像开发 React Native 一样抽离出来。

functionHello() {

return(

<div

style={styles.hello}

>

Hello, World!

</div>

);

}

const styles = {

hello: {

background:'#fff',

display:'flex',

width:'100%',

height:'100%',

alignItems:'center',

justifyContent:'center',

fontSize: 36,

color:'#000'

}

};

const svg = await satori(<Hello />, {

...

});

生成的 SVG 文件如下:

<svg width="600"height="400"viewBox="0 0 600 400"xmlns="http://www.w3.org/2000/svg">

<mask id="satori_om-id">

<rect x="0"y="0"width="600"height="400"fill="#fff"/>

</mask>

<rect x="0"y="0"width="600"height="400"fill="#fff"/>

<path fill="#000"d="M195.7 211.9L198.7 ...略... 212.4Z "/>

</svg>

可以观察到 Satori 把部分 CSS 样式转换成了rect元素的属性,文字也被转换成了path

转换 SVG 到 PNG

按照 Satori 文档的推荐, 我们首先使用 Resvg-js 来将 SVG 转换成 PNG。Resvg 是一个 Rust 实现的 SVG 渲染器,它可以将 SVG 渲染成 PNG。Resvg 的优点是速度快,内存占用小,支持大部分 SVG 特性。

import { Resvg } from'@resvg/resvg-js';

asyncfunctionsvg2png(svg) {

const resvg =newResvg(svg, {

fitTo: {

mode:'width',

value: 600

},

font: {

defaultFontFamily:'Source Han Sans SC Normal'

}

});

const png = resvg.render().asPng();

fs.writeFileSync('./hello.png', png);

}

svg2png(svg);

执行代码就得到了这样一张 PNG 图片。

把以上简单的转换逻辑和 OSS 存储逻辑封装到一个 Node 服务中,这样我们就得到了一个可以通过 HTTP 请求返回图片 URL 的图片生成服务。

下面就是我们图片生成服务生成的一些分享火车票的图片。

Satori 方案相较于之前的 Java 方案有以下收益:

  1. 开发图片模板的效率提升了,新方案前端 1 天就可以搞定原 Java 方案需要开发 3 天的图片模板。

  2. 绘制能力提升了,之前不能开发的动态布局图片,新方案也可以轻松应对。

四、优化

Satori + Resvg 实现 HTML 转 PNG 的方案与我们之前基于java.awt的方案相比,图片开发速度有了提升但是图片生成速度有一些下降。而且随着服务的长时间在线,内存泄露的问题也逐渐显现出来。下面我们来看看如何优化这个方案。

1、速度优化

原来我们基于java.awt的方案生成一张类似的分享火车票图片耗时平均在 500ms, Satori + Resvg 方案刚上线时生成一张图片耗时平均在 900ms, 速度远低于 Java 方案,用户点击分享后需要等待较长的时间才能看到图片,体验较差。

关闭内嵌字体优化

Satori 为了使 SVG 图片在未安装图片中字体的环境中也能正常展示字体,默认启用了内嵌字体优化。这个优化就是上面例子里观察到的字型被转成了path。Satori 执行这个优化需要时间,Resvg 也需要时间来解析这些path。因为我们的系统字体是可控的,所以我们可以关闭这个优化。

const svg = await satori(<div />, {

...otherOptions,

embedFont:false

});

优化后生成一张图片的耗时从 900ms 降低到了 400ms,速度略快于原 Java 方案。

使用 Sharp 替换 Resvg-js

Resvg 只支持输出 PNG 格式的图片,PNG 在某些场景下图片体积要比 JPG 大,在调研支持多格式的图片处理库时发现了Sharp。Sharp 是用 C++ 实现的。它不仅支持转换到 JPG,WEBP 等近十种图片格式,经测试从 SVG 转 PNG 的速度相较 Resvg 也快了近一倍。使用 Sharp 我们不仅提升了图片生成速度,增加多图片格式的支持,还支持了图片的压缩和优化功能,可以产出更小体积的图片,减少了 OSS 存储时间和用户下载时间。

优化后生成一张图片的耗时从 400ms 降低到了 200ms,速度快了近一倍。

2、内存优化

图片格式转换工具因为在运行时需要频繁的分配和释放内存,所以内存泄露的问题比较常见。我们的服务在刚上线时运行 2 天左右内存占用会到 90%。内存泄露不仅影响内存占用,随之而来的内存碎片化问题会导致分配内存效率降低,从而导致图片生成速度下降。下面我们来看看如何优化内存。

使用 jemalloc 内存管理器

jemalloc 是一个内存管理器,它是用 C 实现的,专门用于优化多线程环境下的内存分配和释放。jemalloc 的优点是内存分配和释放效率高,内存碎片化低。我们可以使用 jemalloc 作为 Node.js 的内存管理器,来优化内存的使用。

export LD_PRELOAD=/usr/lib64/libjemalloc.so.1

jemalloc 的使用使内存泄露的速度得到了降低,运行 30 天左右内存占用才会到 90%。问题还没有得到彻底解决,我们还需要进一步优化内存