一、背景
一圖勝千言,圖檔是資訊傳遞的重要載體。當使用者進行社交媒體分享時,圖檔是比文字更加直覺的展示方式。在一些分享的場景可能天然就有圖檔,比如分享一個商品,商品的圖檔就是最直覺的展示方式。但是在一些場景下,可能沒有天然的圖檔,比如分享一個從北京到上海的火車線路,這時候就需要我們自己生成圖檔。
二、方案選型
生成圖檔的方式有很多種, 根據具體的場景可以分為 Web 前端生成圖檔、 用戶端生成圖檔、後端生成圖檔。每種方式都有自己的優缺點,我們根據具體的場景選擇合适的方式。
1、常見的圖檔生成方式
Web 前端
Web 前端有很多可以實作頁面轉圖檔的工具, 我們拿常用的 html2canvas來舉例,html2canvas是一個将網頁内容渲染成圖檔的工具,它可以将網頁的 DOM 元素轉換成 Canvas 元素,然後将 Canvas 元素轉換成圖檔。html2canvas的使用非常簡單,隻需要調用 html2canvas方法傳入需要轉換的 DOM 元素即可。它很适合用在 Web 頁面把頁面中的一部分生成圖檔。這種方式純前端實作不依賴後端,我們有一些活動頁面使用了這種方案。不過 html2canvas有一些局限性,比如隻能在較現代的 Web 環境使用無法一套方案跨多端,不支援跨域資源, 不支援 box-shadow、 filter 等 CSS 特性等。
用戶端
如果我們的應用是一個用戶端應用,比如一個 Android 或者 iOS 應用,我們可以使用用戶端的 API 來生成圖檔。比如 Android 可以使用 View.draw(canvas)方法将 View 繪制到 Canvas 上,然後将 Canvas 轉換成圖檔。iOS 可以使用 UIGraphicsBeginImageContextWithOptions方法将 View 繪制到 Context 上,然後将 Context 轉換成圖檔。這種方式的優點和 html2canvas 類似,純前端實作,使用者所見即所得。局限性也類似,隻能用在各自的系統上,無法跨多端使用。
後端
前端生成圖檔有時會受限于場景無法使用,多端場景也會有重複的開發量。後端生成圖檔在跨端支援方面有先天優勢,可以使用各種語言的圖檔處理庫來生成圖檔。
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 總結了以下缺點:
- 難:該方案需要啟動 Chromium,并使用 Puppeteer 對給定的 HTML 頁面進行截圖。設定這些工具很難實作,而且經常出錯。
- 慢:冷啟動速度非常慢(平均約 4 秒),而且這可能會導緻圖檔運作緩慢或損壞。
- 貴:為了截圖而啟動整個浏覽器并不高效,既昂貴又浪費計算資源。
- 大:Chromium 越來越大,Vercel 的 Serverless Function 裡已經無法容納了。
2、一種新的 Node 後端生成圖檔方式 Satori
Satori 是 Vercel 開源的一套将 HTML 和 CSS 轉換成 SVG 的工具庫。它支援 Node,浏覽器,Web Worker 等環境。它的優點是:
- 容易: 不需要 Headless Chromium,它非常巧妙的把一種文本(HTML ,CSS)轉換成另一種文本(SVG), SVG 可以友善的轉換成各種圖檔。開發方式貼近前端頁面開發,非常直覺且容易上手。
- 快:整個轉換過程非常巧妙的把一種基于文本的描述文檔 HTML 轉換成另一種基于文本的描述文檔 SVG,實際沒有真正繪制 HTML, 是以速度非常快。即便加上 SVG 轉 PNG 的時間,也比 Headless Chromium 快 5 倍左右。
- 輕量: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'; async function html2svg() { 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 一樣抽離出來。
function Hello() { 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'; async function svg2png(svg) { const resvg = new Resvg(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 天就可以搞定原 Java 方案需要開發 3 天的圖檔模闆。
- 繪制能力提升了,之前不能開發的動态布局圖檔,新方案也可以輕松應對。
四、優化
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%。問題還沒有得到徹底解決,我們還需要進一步優化記憶體。
解決第三方庫記憶體洩露問題
經過分析觀察,記憶體洩露發生在 Sharp 轉換 SVG 到 PNG 階段。定位 Sharp 的問題比較困難,我們可以通過程序排程的方式來解決第三方庫記憶體洩露的問題。
為了使主機的負載最大化,我們使用了多程序的方式來運作我們的服務,一台主機上同時運作多個 worker。在保證其他 worker 正常運作的情況下,斷開其中目标 worker 使其不再接收新的請求,随後待 worker 處理完未完成的請求後将其關閉,然後啟動一個新的worker來處理新的請求。這樣我們就可以在不影響整體服務穩定性的情況下釋放掉 Sharp 産生的記憶體洩露。
程序排程方案上線後,我們的服務可以一直穩定運作了,記憶體洩露問題得到了徹底解決。
五、遺憾和驚喜
Satori 方案也有一些缺點,它隻支援 HTML 和 CSS 的一個子集。
- 不支援表單元素,比如 input,select 等。可是一張靜态圖檔支援這些表單元素又有什麼用呢?如果需要完全可以通過 div 實作類似的視覺樣式。
- 不支援 link, style 等标簽。用 JS 完全可以很好的抽象和複用樣式,是以其實也用不到這些标簽。
- 隻支援 flex 布局,不支援 block,grid,table 等其他布局。因為 Satori 目前使用的是和 React Native 相同的布局引擎 Yoga,是以隻支援 flex 布局。不過 React Native 的流行說明 flex 布局在大部分場景下已經足夠了。
- 還有一些其他不支援的特性,但大多在靜态圖檔的場景沒有什麼用,或者可以通過其他方式解決。
另一方面 Satori 雖然隻支援了部分 CSS 樣式,但是還難能可貴的支援了很多進階的CSS 特性:
filter, boxShadow, textShadow, mask, backgroundClip, transform 等。這些特性使得我們可以實作一些比較複雜的圖檔效果。
六、結語
每種圖檔生成方式都有自己适合的場景,Satori + Sharp 在後端生成圖檔的各個方案中在開發效率、生成速度、資源占用上都有較大優勢。我們的圖檔生成服務上線以來已經支撐了 20 多個場景的圖檔生成,生成圖檔超過 1 億張,穩定性和性能都得到了驗證。
作者介紹:
任文龍,2017年加入去哪兒旅行火車票團隊,火車票前端技術委員會成員,主要負責火車票前端技術架構工作。
來源-微信公衆号:Qunar技術沙龍
出處:https://mp.weixin.qq.com/s/vp4v7e67rGbjNCPvjRD6Fw