以前書いたsatori/sharp/satori-htmlでOG画像生成を使ってたら、
やたらsatori-htmlの処理に時間がかかるので、
いろいろ調べてみたときの備忘録(*´ω`*)
使ってるのは前回と同じ、この3つ
- vercel/satori ... ReactNode(VNode)をSVGに変換
- lovell/sharp ... SVGをPNGに変換
- natemoo-re/satori-html ... HTMLをVNodeに変換
遅かったときのコード
最初はこんな感じで、
いくつかの画像をBase64で埋め込む感じに。
import satori from "satori"; import { html } from "satori-html"; import sharp from "sharp"; import fs from "fs"; // 画像を読み込む処理 async function readImageBase64(filePath: string) { // 画像ファイルの読み込み const imageBuffer = await fs.readFileSync(filePath); // 画像ファイルをBase64形式にエンコード const imageBase64 = Buffer.from(imageBuffer).toString("base64"); return `data:image/png;base64,${imageBase64}`; } // 画像の準備 const image1 = await readImageBase64("<image_path1>"); const image2 = await readImageBase64("<image_path2>"); const image3 = await readImageBase64("<image_path3>"); // satori-htmlで文字列をVNodeに変換 const vnode = html(` <div style="display: flex;"> <img src="${image1}" /> <img src="${image2}" /> <img src="${image3}" /> </div> `); // satoriでVNodeをSVGに変換 const svg = await satori( vnode, { width: 1200, height: 630, fonts: [] }, ); // sharpでSVGからPNGに変換 const png = await sharp(Buffer.from(svg)).png().toBuffer();
ただ、const vnode = html()
の部分がかなり遅く。。
satori-htmlはなにをしているのか
satori-htmlのコードを見てみると1ファイルのみ。
やっていることとしては、
- 文字列をultrahtmlでパース
- パースしたHTMLのASTを探索して
VNode
に変換
VNodeはsatoriが受け取れる形式。
interface VNode { type: string; props: { style?: Record<string, any>; children?: string | VNode | VNode[]; [prop: string]: any; }; }
なぜ遅かったのか
Base64形式の画像の文字数は大量で、
大量の文字列をパースしているで時間がかかるよう。。
パース自体も重い処理なので、それはそう。。
改善方法
satori-htmlに渡す文字列を少なくすればよいので、
satori-htmlが返すVNodeに対して置換する感じにしてみた。
// ...略 // 画像の準備 const image1 = await readImageBase64("<image_path1>"); const image2 = await readImageBase64("<image_path2>"); const image3 = await readImageBase64("<image_path3>"); // 対応するキーワードとBase64形式画像のマップ const imageMap = { "@@image1@@": image1, "@@image2@@": image2, "@@image3@@": image3, }; // satori-htmlで文字列をVNodeに変換 const vnode = htmlWithAssets(` <div style="display: flex;"> <img src="@@image1@@" /> <img src="@@image2@@" /> <img src="@@image3@@" /> </div> `, imageMap);
htmlWithAssets
の中身はこんな感じ。
import * as flat from "flat"; import { html } from "satori-html"; function htmlWithAssets( template: string, assetMap: { [key: string]: string; }, ) { // satori-htmlでVNode(JavaScript Object)に変換 const vnode = html(template); // VNodeのネストオブジェクトをflatにする const flatVNode = flat.flatten(vnode); // 各key-valueごとにvalueの中身を見て置換 const assetKeys = Object.keys(assetMap); const replacedEntries = Object.entries(flatVNode).map(v => { if (typeof v[1] == "string") { // valueにassetMapのキーを探す const foundKey = assetKeys.find((x) => v[1].includes(x)); if (foundKey != null) { // 見つけたassetMapのキーを使って置換 return [v[0], v[1].replace(foundKey, assetMap[foundKey])]; } } // 見つからなければそのまま返す return v; }); // Entiresをもとに戻す const applyedObject = Object.fromEntries(replacedEntries); // flattenしたObjectをもとに戻す return flat.unflatten(applyedObject); }
flatten/unflatteには、このライブラリを利用
background-image: url("@@image1@@");
とかもあるので、
単にvalueの一致だとダメなので、includes
を使って探す感じにしてる。
以上!! これで、10分以上かかっても生成できなかったので、
数秒で表示されるようになった(*´ω`*)