遊戲陪玩系統效能優化和設計模式兩方面的知識不管在工作還是面試時都是高頻應用場景,平時大家認為效能優化是一種無序的應用場景,但在筆者看來它是一種有序的應用場景且很多效能優化都是互相鋪墊甚至一帶一路。
從過程趨勢來看,遊戲陪玩系統效能優化可分為網路層面和渲染層面;從結果趨勢來看,遊戲陪玩系統效能優化可分為時間層面和體積層面。簡單來說就是要在訪問網站時使其快準狠地立馬呈現在使用者眼前。
遊戲陪玩系統的效能優化都圍繞著兩大層面兩小層面實現,核心層面是網路層面和渲染層面,輔助層面是時間層面和體積層面,而輔助層面則充滿在核心層面裡。於是筆者通過本文整理出關於遊戲陪玩系統前端效能優化的九大策略和六大指標。當然這些策略和指標都是筆者自己定義,方便通過某種方式為效能優化做一些規範。
所有程式碼示例為了凸顯主題,只展示核心配置程式碼,其他配置並未補上,請自行腦補
九大策略
網路層面
遊戲陪玩系統網路層面的效能優化,無疑是如何讓資源體積更小載入更快,因此筆者從以下四方面做出建議。
- 構建策略:基於構建工具(Webpack/Rollup/Parcel/Esbuild/Vite/Gulp)
- 影像策略:基於影像型別(JPG/PNG/SVG/WebP/Base64)
- 分發策略:基於內容分發網路(CDN)
- 快取策略:基於瀏覽器快取(強快取/協商快取)
上述四方面都是一步接著一步完成,充滿在遊戲陪玩系統整個專案流程裡。構建策略和影像策略處於開發階段,分發策略和快取策略處於生產階段,因此在每個階段都可檢查是否按順序接入上述策略。通過這種方式就能最大限度增加效能優化應用場景。
構建策略
該策略主要圍繞webpack做相關處理,同時也是接入最普遍的遊戲陪玩系統效能優化策略。其他構建工具的處理也是大同小異,可能只是配置上不一致。說到webpack的效能優化,無疑是從時間層面和體積層面入手。
筆者對遊戲陪玩系統兩層面分別做出6個效能優化建議總共12個效能優化建議,為了方便記憶均使用四字詞語概括,方便大家消化。⏱表示減少打包時間,?表示減少打包體積。
- 減少打包時間:縮減範圍、快取副本、定向搜尋、提前構建、並行構建、可視結構
- 減少打包體積:分割程式碼、搖樹優化、動態墊片、按需載入、作用提升、壓縮資源
⏱縮減範圍
配置include/exclude縮小Loader對檔案的搜尋範圍,好處是避免不必要的轉譯。node_modules目錄的體積這麼大,那得增加多少時間成本去檢索所有檔案啊?
include/exclude通常在各大Loader裡配置,src目錄通常作為原始碼目錄,可做如下處理。當然include/exclude可根據實際情況修改。
export default {
// ...
module: {
rules: [{
exclude: /node_modules/,
include: /src/,
test: /\.js$/,
use: "babel-loader"
}]
}};
⏱快取副本
配置cache快取Loader對檔案的編譯副本,好處是再次編譯時只編譯修改過的檔案。未修改過的檔案幹嘛要隨著修改過的檔案重新編譯呢?
大部分Loader/Plugin都會提供一個可使用編譯快取的選項,通常包含cache字眼。以babel-loader和eslint-webpack-plugin為例。
import EslintPlugin from "eslint-webpack-plugin";export default {
// ...
module: {
rules: [{
// ...
test: /\.js$/,
use: [{
loader: "babel-loader",
options: { cacheDirectory: true }
}]
}]
},
plugins: [
new EslintPlugin({ cache: true })
]};
⏱定向搜尋
配置resolve提高檔案的搜尋速度,好處是定向指定必須檔案路徑。若某些第三方庫以常規形式引入可能報錯或希望程式自動索引特定型別檔案都可通過該方式解決。
alias對映模組路徑,extensions表明檔案字尾,noParse過濾無依賴檔案。通常配置alias和extensions就足夠。
export default {
// ...
resolve: {
alias: {
"#": AbsPath(""), // 根目錄快捷方式
"@": AbsPath("src"), // src目錄快捷方式
swiper: "swiper/js/swiper.min.js"
}, // 模組匯入快捷方式
extensions: [".js", ".ts", ".jsx", ".tsx", ".json", ".vue"] // import路徑時檔案可省略字尾名
}};
⏱提前構建
配置DllPlugin將第三方依賴提前打包,好處是將DLL與業務程式碼完全分離且每次只構建業務程式碼。這是一個古老配置,在webpack v2時已存在,不過現在webpack v4+已不推薦使用該配置,因為其版本迭代帶來的效能提升足以忽略DllPlugin所帶來的效益。
DLL意為動態連結庫,指一個包含可由多個程式同時使用的程式碼庫。在前端領域裡可認為是另類快取的存在,它把公共程式碼打包為DLL檔案並存到硬碟裡,再次打包時動態連結DLL檔案就無需再次打包那些公共程式碼,從而提升遊戲陪玩系統構建速度,減少打包時間。
配置DLL總體來說相比其他配置複雜,配置流程可大致分為三步。
首先告知構建指令碼哪些依賴做成DLL並生成DLL檔案和DLL對映表檔案。
import { DefinePlugin, DllPlugin } from "webpack";export default {
// ...
entry: {
vendor: ["react", "react-dom", "react-router-dom"]
},
mode: "production",
optimization: {
splitChunks: {
cacheGroups: {
vendor: {
chunks: "all",
name: "vendor",
test: /node_modules/
}
}
}
},
output: {
filename: "[name].dll.js", // 輸出路徑和檔名稱
library: "[name]", // 全域性變數名稱:其他模組會從此變數上獲取裡面模組
path: AbsPath("dist/static") // 輸出目錄路徑
},
plugins: [
new DefinePlugin({
"process.env.NODE_ENV": JSON.stringify("development") // DLL模式下覆蓋生產環境成開發環境(啟動第三方依賴除錯模式)
}),
new DllPlugin({
name: "[name]", // 全域性變數名稱:減小搜尋範圍,與output.library結合使用
path: AbsPath("dist/static/[name]-manifest.json") // 輸出目錄路徑
})
]};
然後在package.json裡配置執行指令碼且每次構建前首先執行該指令碼打包出DLL檔案。
{
"scripts": {
"dll": "webpack --config webpack.dll.js"
}}
最後連結DLL檔案並告知webpack可命中的DLL檔案讓其自行讀取。使用html-webpack-tags-plugin在打包時自動插入DLL檔案。
import { DllReferencePlugin } from "webpack";import HtmlTagsPlugin from "html-webpack-tags-plugin";export default {
// ...
plugins: [
// ...
new DllReferencePlugin({
manifest: AbsPath("dist/static/vendor-manifest.json") // manifest檔案路徑
}),
new HtmlTagsPlugin({
append: false, // 在生成資源後插入
publicPath: "/", // 使用公共路徑
tags: ["static/vendor.dll.js"] // 資源路徑
})
]};
為了那幾秒鐘的時間成本,筆者建議配置上較好。當然也可使用autodll-webpack-plugin代替手動配置。
⏱並行構建
配置Thread將Loader單程式轉換為多程式,好處是釋放遊戲陪玩系統CPU多核併發的優勢。在使用webpack構建專案時會有大量檔案需解析和處理,構建過程是計算密集型的操作,隨著檔案增多會使構建過程變得越慢。
執行在Node裡的webpack是單執行緒模型,簡單來說就是webpack待處理的任務需一件件處理,不能同一時刻處理多件任務。
遊戲陪玩系統檔案讀寫與計算操作無法避免,能不能讓webpack同一時刻處理多個任務,發揮多核CPU電腦的威力以提升構建速度呢?thread-loader來幫你,根據CPU個數開啟執行緒。
在此需注意一個問題,若專案檔案不算多就不要使用該效能優化建議,畢竟開啟多個執行緒也會存在效能開銷。
import Os from "os";export default {
// ...
module: {
rules: [{
// ...
test: /\.js$/,
use: [{
loader: "thread-loader",
options: { workers: Os.cpus().length }
}, {
loader: "babel-loader",
options: { cacheDirectory: true }
}]
}]
}};
⏱可視結構
配置BundleAnalyzer分析打包檔案結構,好處是找出遊戲陪玩系統導致體積過大的原因。從而通過分析原因得出優化方案減少構建時間。BundleAnalyzer是webpack官方外掛,可直觀分析打包檔案的模組組成部分、模組體積佔比、模組包含關係、模組依賴關係、檔案是否重複、壓縮體積對比等視覺化資料。
可使用webpack-bundle-analyzer配置,有了它,我們就能快速找到相關問題。
import { BundleAnalyzerPlugin } from "webpack-bundle-analyzer";export default {
// ...
plugins: [
// ...
BundleAnalyzerPlugin()
]};
?分割程式碼
分割遊戲陪玩系統各個模組程式碼,提取相同部分程式碼,好處是減少重複程式碼的出現頻率。webpack v4使用splitChunks替代CommonsChunksPlugin實現程式碼分割。
splitChunks配置較多,詳情可參考官網,在此筆者貼上常用配置。
export default {
// ...
optimization: {
runtimeChunk: { name: "manifest" }, // 抽離WebpackRuntime函式
splitChunks: {
cacheGroups: {
common: {
minChunks: 2,
name: "common",
priority: 5,
reuseExistingChunk: true, // 重用已存在程式碼塊
test: AbsPath("src")
},
vendor: {
chunks: "initial", // 程式碼分割型別
name: "vendor", // 程式碼塊名稱
priority: 10, // 優先順序
test: /node_modules/ // 校驗檔案正規表示式
}
}, // 快取組
chunks: "all" // 程式碼分割型別:all全部模組,async非同步模組,initial入口模組
} // 程式碼塊分割
}};
?搖樹優化
刪除遊戲陪玩系統中未被引用程式碼,好處是移除重複程式碼和未使用程式碼。搖樹優化首次出現於rollup,是rollup的核心概念,後來在webpack v2裡借鑑過來使用。
搖樹優化只對ESM規範生效,對其他模組規範失效。搖樹優化針對靜態結構分析,只有import/export才能提供靜態的匯入/匯出功能。因此在編寫業務程式碼時必須使用ESM規範才能讓搖樹優化移除重複程式碼和未使用程式碼。
在webpack裡只需將打包環境設定成生產環境就能讓搖樹優化生效,同時業務程式碼使用ESM規範編寫,使用import匯入模組,使用export匯出模組。
export default {
// ...
mode: "production"};
?動態墊片
通過墊片服務根據UA返回當前瀏覽器程式碼墊片,好處是無需將繁重的程式碼墊片打包進去。每次構建都配置@babel/preset-env和core-js根據某些需求將Polyfill打包進來,這無疑又為程式碼體積增加了貢獻。
@babel/preset-env提供的useBuiltIns可按需匯入Polyfill。
- false:無視target.browsers將所有Polyfill載入進來
- entry:根據target.browsers將部分Polyfill載入進來(僅引入有瀏覽器不支援的Polyfill,需在入口檔案import
“core-js/stable”)
- usage:根據target.browsers和檢測程式碼裡ES6的使用情況將部分Polyfill載入進來(無需在入口檔案import
“core-js/stable”)
在此推薦大家使用動態墊片。動態墊片可根據瀏覽器UserAgent返回當前瀏覽器Polyfill,其思路是根據瀏覽器的UserAgent從browserlist查詢出當前瀏覽器哪些特性缺乏支援從而返回這些特性的Polyfill。對這方面感興趣的同學可參考polyfill-library和polyfill-service的原始碼。
使用html-webpack-tags-plugin在打包時自動插入動態墊片。
import HtmlTagsPlugin from "html-webpack-tags-plugin";export default {
plugins: [
new HtmlTagsPlugin({
append: false, // 在生成資源後插入
publicPath: false, // 使用公共路徑
tags: ["] // 資源路徑
})
]};
?按需載入
將遊戲陪玩系統路由頁面/觸發性功能單獨打包為一個檔案,使用時才載入,好處是減輕首屏渲染的負擔。因為專案功能越多其打包體積越大,導致首屏渲染速度越慢。
遊戲陪玩系統首屏渲染時只需對應JS程式碼而無需其他JS程式碼,所以可使用按需載入。webpack v4提供模組按需切割載入功能,配合import()可做到首屏渲染減包的效果,從而加快首屏渲染速度。只有當觸發某些功能時才會載入當前功能的JS程式碼。
webpack v4提供魔術註解命名切割模組,若無註解則切割出來的模組無法分辨出屬於哪個業務模組,所以一般都是一個業務模組共用一個切割模組的註解名稱。
const Login = () => import( /* webpackChunkName: "login" */ "../../views/login");const Logon = () => import( /* webpackChunkName: "logon" */ "../../views/logon");
執行起來控制檯可能會報錯,在package.json的babel相關配置裡接入@babel/plugin-syntax-dynamic-import即可。
{
// ...
"babel": {
// ...
"plugins": [
// ...
"@babel/plugin-syntax-dynamic-import"
]
}}
?作用提升
分析模組間依賴關係,把打包好的模組合併到一個函式中,好處是減少函式宣告和記憶體花銷。作用提升首次出現於rollup,是rollup的核心概念,後來在webpack v3裡借鑑過來使用。
在未開啟作用提升前,構建後的遊戲陪玩系統程式碼會存在大量函式閉包。由於模組依賴,通過webpack打包後會轉換成IIFE,大量函式閉包包裹程式碼會導致打包體積增大(模組越多越明顯)。在執行程式碼時建立的函式作用域變多,從而導致更大的記憶體開銷。
在開啟作用提升後,構建後的遊戲陪玩系統程式碼會按照引入順序放到一個函式作用域裡,通過適當重新命名某些變數以防止變數名衝突,從而減少函式宣告和記憶體花銷。
在webpack裡只需將打包環境設定成生產環境就能讓作用提升生效,或顯式設定concatenateModules。
export default {
// ...
mode: "production"};// 顯式設定export default {
// ...
optimization: {
// ...
concatenateModules: true }};
?壓縮資源
壓縮HTML/CSS/JS程式碼,壓縮字型/影像/音訊/視訊,好處是更有效減少遊戲陪玩系統打包體積。極致地優化程式碼都有可能不及優化一個資原始檔的體積更有效。
針對HTML程式碼,使用html-webpack-plugin開啟壓縮功能。
import HtmlPlugin from "html-webpack-plugin";export default {
// ...
plugins: [
// ...
HtmlPlugin({
// ...
minify: {
collapseWhitespace: true,
removeComments: true } // 壓縮HTML
})
]};
針對CSS/JS程式碼,分別使用以下外掛開啟壓縮功能。其中OptimizeCss基於cssnano封裝,Uglifyjs和Terser都是webpack官方外掛,同時需注意壓縮JS程式碼需區分ES5和ES6。
- optimize-css-assets-webpack-plugin:壓縮CSS程式碼
- uglifyjs-webpack-plugin:壓縮ES5版本的JS程式碼
- terser-webpack-plugin:壓縮ES6版本的JS程式碼
import OptimizeCssAssetsPlugin from "optimize-css-assets-webpack-plugin";import TerserPlugin from "terser-webpack-plugin";import UglifyjsPlugin from "uglifyjs-webpack-plugin";const compressOpts = type => ({
cache: true, // 快取檔案
parallel: true, // 並行處理
[`${type}Options`]: {
beautify: false,
compress: { drop_console: true }
} // 壓縮配置});const compressCss = new OptimizeCssAssetsPlugin({
cssProcessorOptions: {
autoprefixer: { remove: false }, // 設定autoprefixer保留過時樣式
safe: true // 避免cssnano重新計算z-index
}});const compressJs = USE_ES6 ? new TerserPlugin(compressOpts("terser"))
: new UglifyjsPlugin(compressOpts("uglify"));export default {
// ...
optimization: {
// ...
minimizer: [compressCss, compressJs] // 程式碼壓縮
}};
針對遊戲陪玩系統字型/音訊/視訊檔案,還真沒相關Plugin供我們使用,就只能拜託大家在釋出專案到生產服前使用對應的壓縮工具處理了。針對影像檔案,大部分Loader/Plugin封裝時均使用了某些影像處理工具,而這些工具的某些功能又託管在國外伺服器裡,所以導致經常安裝失敗。
鑑於此,筆者花了一點小技巧開發了一個Plugin用於配合webpack壓縮影像,詳情請參考tinyimg-webpack-plugin。
import TinyimgPlugin from "tinyimg-webpack-plugin";export default {
// ...
plugins: [
// ...
TinyimgPlugin()
]};
分發策略
該策略主要圍繞遊戲陪玩系統內容分發網路做相關處理,同時也是接入成本較高的效能優化策略,需足夠資金支援。
雖然接入成本較高,但大部分企業都會購買一些CDN伺服器,所以在部署的事情上就不用過分擔憂,儘管使用就好。該策略儘量遵循以下兩點就能發揮CDN最大作用。
- 所有靜態資源走CDN:開發階段確定哪些檔案屬於靜態資源
- 把靜態資源與主頁面置於不同域名下:避免請求帶上Cookie
內容分發網路簡稱CDN,指一組分佈在各地儲存資料副本並可根據就近原則滿足資料請求的伺服器。其核心特徵是快取和回源,快取是把資源複製到CDN伺服器裡,回源是資源過期/不存在就向上層伺服器請求並複製到CDN伺服器裡。
使用CDN可降低網路擁塞,提高使用者訪問響應速度和命中率。構建在現有網路基礎上的智慧虛擬網路,依靠部署在各地伺服器,通過中心平臺的排程、負載均衡、內容分發等功能模組,使遊戲陪玩系統使用者就近獲取所需資源,這就是CDN的終極使命。
基於CDN的就近原則所帶來的優點,可將遊戲陪玩系統所有靜態資源全部部署到CDN伺服器裡。那靜態資源包括哪些檔案?通常來說就是無需伺服器產生計算就能得到的資源,例如不常變化的樣式檔案、指令碼檔案和多媒體檔案(字型/影像/音訊/視訊)等。
快取策略
該策略主要圍繞遊戲陪玩系統快取做相關處理,同時也使接入成本最低的效能優化策略。其顯著減少網路傳輸所帶來的損耗,提升網頁訪問速度,是一種很值得使用的效能優化策略。
通過下圖可知,為了讓瀏覽器快取發揮最大作用,該策略儘量遵循以下五點就能發揮瀏覽器快取最大作用。
- 考慮拒絕一切快取策略:Cache-Control:no-store
- 考慮資源是否每次向伺服器請求:Cache-Control:no-cache
- 考慮資源是否被代理伺服器快取:Cache-Control:public/private
- 考慮資源過期時間:Expires:t/Cache-Control:max-age=t,s-maxage=t
- 考慮協商快取:Last-Modified/Etag
快取策略通過設定HTTP報文實現,在形式上分為強快取/強制快取和協商快取/對比快取。為了方便對比,筆者將某些細節使用圖例展示,相信你有更好的理解。
整個快取策略機制很明瞭,先走強快取,若命中失敗才走協商快取。若命中強快取,直接使用強快取;若未命中強快取,傳送請求到遊戲陪玩系統伺服器檢查是否命中協商快取;若命中協商快取,伺服器返回304通知瀏覽器使用本地快取,否則返回最新資源。
有兩種較常用的應用場景值得使用快取策略一試,當然更多應用場景都可根據專案需求制定。
- 頻繁變動資源:設定Cache-Control:no-cache,使瀏覽器每次都傳送請求到伺服器,配合Last-Modified/ETag驗證資源是否有效
- 不常變化資源:設定Cache-Control:max-age=31536000,對檔名雜湊處理,當程式碼修改後生成新的檔名,當HTML檔案引入檔名發生改變才會下載最新檔案
渲染層面
遊戲陪玩系統渲染層面的效能優化,無疑是如何讓程式碼解析更好執行更快。因此筆者從以下五方面做出建議。
- CSS策略:基於CSS規則
- DOM策略:基於DOM操作
- 阻塞策略:基於指令碼載入
- 迴流重繪策略:基於迴流重繪
- 非同步更新策略:基於非同步更新
上述五方面都是編寫遊戲陪玩系統程式碼時完成,充滿在整個專案流程的開發階段裡。因此在開發階段需時刻注意以下涉及到的每一點,養成良好的開發習慣,效能優化也自然而然被使用上了。
渲染層面的效能優化更多表現在遊戲陪玩系統編碼細節上,而並非實體程式碼。簡單來說就是遵循某些編碼規則,才能將渲染層面的效能優化發揮到最大作用。
迴流重繪策略在渲染層面的效能優化裡佔比較重,也是最常規的效能優化之一。
- 避免出現超過三層的巢狀規則
- 避免為ID選擇器新增多餘選擇器
- 避免使用標籤選擇器代替類選擇器
- 避免使用通配選擇器,只對目標節點宣告規則
- 避免重複匹配重複定義,關注可繼承屬性
- 快取DOM計算屬性
- 避免過多DOM操作
- 使用DOMFragment快取批量化DOM操作
- 快取DOM計算屬性
- 使用類合併樣式,避免逐條改變樣式
- 使用display控制DOM顯隱,將DOM離線化
六大指標
筆者根據遊戲陪玩系統效能優化的重要性和實際性劃分出九大策略和六大指標,其實它們都是一條條活生生的效能優化建議。有些效能優化建議接不接入影響都不大,因此筆者將九大策略定位高於六大指標。針對九大策略還是建議在開發階段和生產階段接入,在專案覆盤時可將六大指標的條條框框根據實際應用場景接入。
六大指標基本囊括大部分效能優化細節,可作為九大策略的補充。筆者根據每條效能優化建議的特徵將指標劃分為以下六方面。
- 載入優化:資源在載入時可做的效能優化
- 執行優化:資源在執行時可做的效能優化
- 渲染優化:資源在渲染時可做的效能優化
- 樣式優化:樣式在編碼時可做的效能優化
- 指令碼優化:指令碼在編碼時可做的效能優化
- V8引擎優化:針對V8引擎特徵可做的效能優化
指令碼優化
V8引擎優化
總結
遊戲陪玩系統效能優化作為老生常談的知識,必然會在工作或面試時遇上。很多時候不是想到某條效能優化建議就去做或答,而是要對這方面有一個整體認知,知道為何這樣設計,這樣設計的目的能達到什麼效果。
遊戲陪玩系統效能優化不是通過一篇文章就能全部講完,本文能到給大家的就是一個方向一種態度,學以致用唄,希望閱讀完本文會對你有所幫助。
本文轉載自網路,轉載僅為分享乾貨知識,如有侵權歡迎聯絡雲豹科技進行刪除處理
原文連結:
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69996194/viewspace-2794045/,如需轉載,請註明出處,否則將追究法律責任。