next image export optimizer with blur image in data url format
因爲最近自己寫的靜態部落格產生器(上次zettelkasten card and blog static website generator),有用到 next image export optimizer 這個套件遇到一些小問題,所以特別來記錄一下
前言
先來講爲何我會用到這個套件,其實原因也很單純,因爲根據瀏覽裝置不同,其實你的image並不需要那麼大解析度,就可以顯示完美的影像,多餘的像素沒什麼作用,太大也只是浪費網路頻寬,在網路速度不快的時候,這點算是滿重要的,畢竟你想像一下,你在瀏覽一個網站的時候,部分頁面有出來,然後有些內容還在loading,等了幾秒後,很多人可能就懶得等下去,直接離開,甚至更吊人胃口的是,在你想要離開的時候,部分內容開始loading了,但是又開始pending了,這種感覺很磨人~ 不過以後如果網路速度再繼續飆升或許這些也都不在重要了吧。 就像是很久以前記憶體十足珍貴,大家都要想破頭怎麼減少記憶體使用量,看看如今記憶體根本不怕用完,要多少~都給你~。
轉回正題,造成這種部分畫面出來很慢的狀況,通常是網站有過多很大的圖片或是其他靜態檔,造成requests pending,都被佔滿了,或是你的component依靠API才能render,那就是另外問題,今天的靜態網站不會有這個狀況,所以資源loading問題其實蠻需要重視的,決定什麼東西要先preload,什麼可以延後loading,就顯得很重要了。因此這個套件是我拿來做不同瀏覽裝置提供不同size的圖片用的,這樣可以節省頻寬並減少上面那個狀況。
遇到的問題
其實這個套件用起來幾乎可以說是不太需要設定什麼,針對靜態網站也有支援,本質上他其實就是包裝 Next.js 內建的 Image 做了點包裝,用 sharp js 幫你產生不同size的圖片,到目前爲止聽起來很完美吧~的確使用上沒啥大問題,唯一就是他幫我產生的 blur image,他用了一個滿hack的方式下面是部分 source code
// Currently, we have to handle the blurDataURL ourselves as the new Image component
// is expecting a base64 encoded string, but the generated blurDataURL is a normal URL
const blurStyle =
placeholder === "blur" &&
!isSVG &&
automaticallyCalculatedBlurDataURL &&
automaticallyCalculatedBlurDataURL.startsWith("/") &&
!blurComplete
? {
backgroundSize: style?.objectFit || "cover",
backgroundPosition: style?.objectPosition || "50% 50%",
backgroundRepeat: "no-repeat",
backgroundImage: `url("${automaticallyCalculatedBlurDataURL}")`,
}
: undefined;
好傢伙,我追源碼也看到了他自己的註解,因爲他產生的 blur data url 是一般 url link,所以他用了 css background image 來做出類似效果,不過還是有個問題,應該他是一般url,這代表他還是會被排進去request queue裡面,畢竟要做額外的http requests,是不是在想這樣有什麼問題? 讀取圖片本來就需要request不是嗎?
讓我來述說一下,你有機會遇到原本對應的blur image 都還沒load好,原本的圖片反而先load好的狀況。 那這個blur image 豈不是多餘,然後視覺上就是會看到圖片是空白的問題? 不知道爲何沒人發 issue,所以我想說來看看爲何作者自己都寫了註解,但是這麼久爲何都沒改善? 雖然問題不大就是了,反正網路速度不要慢到哭,其實應該都察覺不太到這個問題。
嘗試hack的方式
一開始想當然就是先看看source code 是不是有什麼地方需要改的,沒想到不讀不知道,讀了才嚇到啊,各位
我還是第一次看到 example 裡面的 code 會被拿來產生最後的 library output。。。 最上面的code也是在example 資料夾裡面找到的,我還花了點時間確認是不是我找錯了,經過實驗驗證確實就是example裡面是有用到的,我想了想,反正我是先來看code哪邊可以改,就先不理這個問題了XD
有興趣的可以去看 tsconfig.json,主要是下面部分決定他會去用example裡面的code
{
..,
"files": [
"example/src/ExportedImage.tsx",
"example/src/legacy/ExportedImage.tsx",
"example/environment.d.ts"
]
}
在開始修改前,我這邊是直接乾脆 npm install [local repo path]
的方式,讓他吃我本地的修改。 其實也有另外的方式就是用 npm link
,看大家喜歡哪個方式自己挑。
切回正題, 找到最後會看到 root cause 就是
const automaticallyCalculatedBlurDataURL = useMemo(() => {
if (blurDataURL) {
// use the user provided blurDataURL if present
return blurDataURL;
}
// check if the src is specified as a local file -> then it is an object
const isStaticImage = typeof src === "object";
let _src = isStaticImage ? src.src : src;
if (unoptimized === true) {
// return the src image when unoptimized
return _src;
}
// Check if the image is a remote image (starts with http or https)
if (_src.startsWith("http")) {
return imageURLForRemoteImage({ src: _src, width: 10, basePath });
}
return generateImageURL(_src, 10, basePath);
}, [blurDataURL, src, unoptimized, basePath]);
這段就是決定 blur image 要怎麼顯示,因爲他最後就是一般 url , 我想說要不然我就在這邊做了額外寫function去包裝 imageToDataURL(generateImageURL(_src, 10, basePath);
類似這樣,原本我測試了一下好像可以正常運作,但是後來發現因爲他的 ExportedImage,並非是單純 server side render 完給 client 而已, client 端也會執行,會看到第一次render正常,但是如果在client端做了頁面切換,這時候是單純全部都是client side,會遇到問題。。 所以就算我特意用了
if (typeof window === 'undefined') {
}
之類的手法,去避免爆掉,還是會遇到client端無法用轉成 data url 的問題,因爲 fs的功能只在 nodejs 端,後來又做了各種hack,感覺有點亂,想了想,還是不要改library source code,直接在我的 static blog 這邊想辦法解決,可能比較單純(比較容易決定server side執行轉換成 data url 這件事)。
最後決定
如上面所述,打算把一些 hack 的 code 用到我自己的 repo,因爲暫時想不到漂亮做法,而且主要問題是作者的寫法,是同時又作爲 client component,會被侷限住。 主要方針就是在 server component的時候,做轉換 data URL, 遇到 client 端的問題,就必須抽一層在server 端做掉,在mix client component。
其實實作不難,主要就是去抓 next image export optmizer套件產生的 blur image,難的是要瞭解 Next.js 的 flow
export function imageToDataURL(imagePath: string) {
imagePath = generateImageURL(imagePath, 10, '')
imagePath = process.env.nextImageExportOptimizer_imageFolderPath + imagePath
if (typeof window === 'undefined') {
if (imagePath in imagePathToDataURLMap) {
return imagePathToDataURLMap[imagePath]
}
const fs = require('fs')
const path = require('path')
// Read the image file as a binary buffer
const fileBuffer = fs.readFileSync(imagePath)
// Determine the MIME type from the file extension
const mimeType = `image/${path.extname(imagePath).slice(1)}`
// Convert the buffer to a base64 string
const base64 = fileBuffer.toString('base64')
// Create the Data URL
const dataURL = `data:${mimeType};base64,${base64}`
imagePathToDataURLMap[imagePath] = dataURL
return dataURL
}
return imagePathToDataURLMap[imagePath] || ''
}
基本上如果你的是純 server component 那沒什麼問題直接像下面這樣使用,在 blurDATAURL 那邊加入 imageToDataURL 轉換過的結果就好
<ExportedImage
src={picture}
blurDataURL={imageToDataURL(picture)}
fill
sizes="(max-width: 768px) 60vw, (max-width: 1200px) 50vw"
style={{ objectFit: 'cover', objectPosition: '50% 80%' }}
alt={name}
className="rounded-tl-[50%] rounded-br-[50%]"
/>
萬一是你的是 client component 呢? 也就是你的檔案開頭有 'use client'
, 很遺憾,你只能抽出來用參數的方式把 blurDataURL 帶進去,舉例
'use client'
const CoverImage = ({ title, src, slug, link, blurDataURL }: Props) => {
return (<ExportedImage
src={src}
blurDataURL={blurDataURL || imageToDataURL(src)}
/>)
}
這時候的 imageToDataURL 會被在 browser 環境執行,因爲你的 nodejs 相關的 library 會無法運作,所以你必須在上一層(必須是server component)帶入 blurDataURL。 在 Next.js 應該是滿容易遇到 server component mix client component 的情況的,這需要多注意。
以我原本blog的首頁,會看到很多blur image 的request像下面圖片這樣
經過這次的優化後
都是在 memory 裡面,已經事先被 load 了~ 棒棒!
希望有給有用到 next image export optimizer 這個套件,遇到跟我類似需求的人一點靈感。該來繼續想怎麼搞我的部落格 CV page了,突然轉彎來搞優化耗了不少時間。