Prerender 是由 Taro CLI 提供的在小程序端提高頁(yè)面初始化渲染速度的一種技術(shù),它的實(shí)現(xiàn)原理和服務(wù)端渲染(Server-side Rendering)一樣:將頁(yè)面初始化的狀態(tài)直接渲染為無(wú)狀態(tài)(dataless)的 wxml,在框架和業(yè)務(wù)邏輯運(yùn)行之前執(zhí)行渲染流程。經(jīng)過(guò) Prerender 的頁(yè)面初始渲染速度通常會(huì)和原生小程序一致甚至更快。
Taro Next 在一個(gè)頁(yè)面加載時(shí)需要經(jīng)歷以下步驟:
setData()
驅(qū)動(dòng)頁(yè)面渲染和原生小程序或編譯型小程序框架相比,步驟 1 和 步驟 2 是多余的。如果頁(yè)面的業(yè)務(wù)邏輯代碼沒(méi)有性能問(wèn)題的話,大多數(shù)性能瓶頸出在步驟 2 的 setData()
上:由于初始化渲染是頁(yè)面的整棵虛擬 DOM 樹,數(shù)據(jù)量比較大,因此 setData()
需要傳遞一個(gè)比較大的數(shù)據(jù),導(dǎo)致初始化頁(yè)面時(shí)會(huì)一段白屏的時(shí)間。這樣的情況通常發(fā)生在頁(yè)面初始化渲染的 wxml 節(jié)點(diǎn)數(shù)比較大或用戶機(jī)器性能較低時(shí)發(fā)生。
使用 Prerender 非常簡(jiǎn)單,你可以找到項(xiàng)目根目錄下的 config
文件夾,根據(jù)你的項(xiàng)目情況更改 index.js
/dev.js
/prod.js
三者中的任意一個(gè)項(xiàng)目配置,在編譯時(shí)
Taro CLI 會(huì)根據(jù)你的配置自動(dòng)啟動(dòng) prerender:
// /project/config/prod.js
const config = {
...
mini: {
prerender: {
match: 'pages/shop/**', // 所有以 `pages/shop/` 開頭的頁(yè)面都參與 prerender
include: ['pages/any/way/index'], // `pages/any/way/index` 也會(huì)參與 prerender
exclude: ['pages/shop/index/index'] // `pages/shop/index/index` 不用參與 prerender
}
}
};
module.exports = config
完整 Prerender 配置可參看下表:
參數(shù) | 類型 | 默認(rèn)值 | 必填 | 說(shuō)明 |
---|---|---|---|---|
match | string string[]
|
否 | glob 字符串或 glob 字符串?dāng)?shù)組,能匹配到本參數(shù)的頁(yè)面會(huì)加入 prerender | |
include | Array Array
|
[]
|
否 | ?頁(yè)面路徑與數(shù)組中字符串完全一致的會(huì)加入 prerender |
exclude | string[]
|
[]
|
否 | ?頁(yè)面路徑與數(shù)組中字符串完全一致的不會(huì)加入 prerender |
mock | Record
|
否 | 在 prerender 環(huán)境中運(yùn)行的全局變量,鍵名為變量名,鍵值為變量值 | |
console | boolean
|
false
|
否 | 在 prerender 過(guò)程中 console 打印語(yǔ)句是否執(zhí)行 |
transformData | Function
|
否 | 自定義虛擬 DOM 樹處理函數(shù),函數(shù)返回值會(huì)作為 transformXML 的參數(shù) |
|
transformXML | Function
|
否 | 自定義 XML 處理函數(shù),函數(shù)返回值是 Taro 運(yùn)行時(shí)初始化結(jié)束前要渲染的 wxml |
在表中有用到的類型:
// PageConfig 是開發(fā)者在 prerender.includes 配置的頁(yè)面參數(shù)
interface PageConfig {
path: string // 頁(yè)面路徑
params: Record<string, unknown> // 頁(yè)面的路由參數(shù),對(duì)應(yīng) `getCurrentInstance().router.params`
}
// DOM 樹數(shù)據(jù),Taro 通過(guò)遍歷它動(dòng)態(tài)渲染數(shù)據(jù)
interface MiniData {
["cn" /* ChildNodes */]: MiniData[]
["nn" /* NodeName */]: string
["cl" /* Class */]: string
["st" /* Style */]: string
["v" /* NodeValue */]: string
uid: string
[prop: string]: unknown
}
type transformData = (data: MiniData, config: PageConfig) => MiniData
type transformXML = (
data: MiniData,
config: PageConfig,
xml: string // 內(nèi)置 xml 轉(zhuǎn)換函數(shù)已經(jīng)處理好了的 xml 字符串
) => string
Prerender 的所有配置選項(xiàng)都是選填的,就多數(shù)情況而言只需要關(guān)注 match
、include
、exclude
三個(gè)選項(xiàng),match
和 include
至少填寫一個(gè)才能匹配到預(yù)渲染頁(yè)面,三者可以共存,當(dāng)匹配沖突時(shí)優(yōu)先級(jí)為 match
< include
< exclude
。
和所有技術(shù)一樣,Prerender 并不是銀彈,使用 Prerender 之后將會(huì)有以下的 trade-offs 或限制:
hydrate
),預(yù)渲染的頁(yè)面不會(huì)相應(yīng)任何操作。componentDidMount()
(React)/ready()
(Vue) 這樣的生命周期,這點(diǎn)和服務(wù)端渲染一致。如果有處理數(shù)據(jù)的需求,可以把生命周期提前到 static getDerivedStateFromProps()
(React) 或 created()
(Vue)。在預(yù)渲染容器有一個(gè)名為 PRERENDER
的全局變量,它的值為 true
。你可以通過(guò)判斷這個(gè)變量是否存在,給預(yù)渲染時(shí)期單獨(dú)編寫業(yè)務(wù)邏輯:
if (typeof PRERENDER !== 'undefined') { // 以下代碼只會(huì)在預(yù)渲染中執(zhí)行
// do something
}
對(duì)于任意一個(gè)原生組件,如果不需要它在 Prerender 時(shí)中顯示,可以把組件的 disablePrerender
屬性設(shè)置為 true
,這個(gè)組件和它的子孫都不會(huì)被渲染為 wxml 字符串。
/* id 為 test 的組件和它的子孫在預(yù)渲染時(shí)都不會(huì)顯示 */
<View id="test" disablePrerender>
...children
View>
當(dāng)默認(rèn)預(yù)渲染的結(jié)果不滿足你的預(yù)期時(shí),Taro 提供了兩個(gè)配置項(xiàng)自定義預(yù)渲染內(nèi)容。
Prerender 配置中的 transformData()
對(duì)需要進(jìn)行渲染的虛擬 DOM 進(jìn)行操作:
const config = {
...
mini: {
prerender: {
match: 'pages/**',
tranformData (data, { path }) {
if (path === 'pages/video/index') {
// 如果是頁(yè)面是 'page/video/index' 頁(yè)面只預(yù)渲染一個(gè) video 組件
// 關(guān)于 data 的數(shù)據(jù)結(jié)構(gòu)可以參看上文的數(shù)據(jù)類型簽名
data.nn = 'video'
data.cn = []
data.src = 'https://v.qq.com/iframe/player.html?vid=y08180lrvth&tiny=0&auto=0'
return data
}
return data
}
}
}
}
Prerender 配置中的 transformXML()
可以自定義預(yù)渲染輸出的 wxml:
const config = {
...
mini: {
prerender: {
match: 'pages/**',
tranformXML (data, { path }, xml) {
if (path === 'pages/video/index') {
// 如果是頁(yè)面是 'page/video/index' 頁(yè)面只預(yù)渲染一個(gè) video 組件
return ``
}
return xml
}
}
}
}
一般而言,用戶只需要看到首屏頁(yè)面,但實(shí)際上頁(yè)面初次渲染的我們構(gòu)建的業(yè)務(wù)邏輯有可能會(huì)把頁(yè)面的所有內(nèi)容都渲染,而 Taro 初始渲染慢的原因在于首次傳遞的數(shù)據(jù)量過(guò)大,因此可以調(diào)整我們的業(yè)務(wù)邏輯達(dá)到只渲染首屏的目的:
class SomePage extends Component {
state = {
mounted: false
}
componentDidMount () {
// 等待組件載入,先渲染了首屏我們?cè)黉秩酒渌鼉?nèi)容,降低首次渲染的數(shù)據(jù)量
// 當(dāng) mounted 為 true 時(shí),CompA, B, C 的 DOM 樹才會(huì)作為 data 參與小程序渲染
// 注意我們需要在 `componentDidMount()` 這個(gè)周期做這件事(對(duì)應(yīng) Vue 的 `ready()`),更早的生命周期 `setState()` 會(huì)與首次渲染的數(shù)據(jù)一起合并更新
// 使用 nextTick 確保本次 setState 不會(huì)和首次渲染合并更新
Taro.nextTick(() => {
this.setState({
mounted: true
})
})
}
render () {
return <View>
<FirstScreen /> /* 假設(shè)我們知道這個(gè)組件會(huì)把用戶的屏幕全部占據(jù) */
{this.state.mounted && <React.Fragment> /* CompA, B, C 一開始并不會(huì)在首屏中顯示 */
<CompA />
<CompB />
<CompC />
React.Fragment>}
View>
}
}
這樣的優(yōu)化除了加快首屏渲染以及 hydrate
的速度,還可以降低 Prerender 的所增加的 wxml 體積。當(dāng)你的優(yōu)化做得足夠徹底時(shí),你會(huì)發(fā)現(xiàn)多數(shù)情況下并不需要 Prerender。
更多建議: