優化無止境,愛奇藝中後臺 Web 應用性能優化實踐

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"愛奇藝視頻生產智能雲平臺系統在今年進行了一次","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"重大升級","attrs":{}},{"type":"text","text":",前端團隊也趁此機會將","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"底層技術架構","attrs":{}},{"type":"text","text":"從三年前的 Arm.js(內部MVC框架)+ Java BFF + Velocity 模板完全切換到了 Vue.js + Node.js BFF 的技術棧。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"新的前端應是一個擁有超過","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"十個業務模塊","attrs":{}},{"type":"text","text":"的單頁面應用,每個模塊已經通過路由懶加載進行了拆分,同時公共的第三方依賴也拆分到了單獨的 Vendor 文件。不過在上線試用初期,用戶還是普遍反饋頁面打開速度較老版本有比較明顯的下降,存在幾秒鐘不等的白屏等待時間。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲了提升用戶體驗和使用效率,團隊內部對新版前端應用進行了多次優化,最終效果","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"提升非常顯著","attrs":{}},{"type":"text","text":"。本文的主要內容就是針對中後臺 Web 應用性能的","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"分析思路","attrs":{}},{"type":"text","text":"及","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"解決方案","attrs":{}},{"type":"text","text":"的總結分享。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"問題梳理","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們先通過提問題的方式,從","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"資源文件加載、頁面渲染性能、接口響應速度","attrs":{}},{"type":"text","text":"等三個方面分別列出了一些可能存在性能瓶頸的環節。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在一個複雜的 Web 應用中,通常會依賴很多 JS/CSS/Images 等資源文件。如何在最短時間內獲取頁面所需的最小資源,我們需要考慮以下幾個問題:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"源碼中有無","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"冗餘的模塊","attrs":{}},{"type":"text","text":"?是否進行了","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"壓縮、合併","attrs":{}},{"type":"text","text":"等操作?","attrs":{}}]}],"attrs":{}}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"服務器響應及網絡傳輸速度是否正常?有沒有最大化利用瀏覽器的併發請求?","attrs":{}}]}],"attrs":{}}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"資源文件的緩存策略是否合理?是否每次發佈上線都需要重新請求所有文件?","attrs":{}}]}],"attrs":{}}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首次頁面渲染是否下載了不必要的資源文件?每次渲染所需的資源文件能不能提前加載?","attrs":{}}]}],"attrs":{}}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"頁面渲染問題","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由於 JS 是在單線程中執行,而 Vue.js 框架的大部分渲染任務都在瀏覽器端完成。爲了解決白屏、卡頓等問題,我們需要考慮以下幾個問題:","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"是否可以通過骨架屏等方式提前渲染核心佈局?","attrs":{}}]}],"attrs":{}}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"主線程是否存在非常耗時的長任務?是否可以進行任務分片、延遲渲染?","attrs":{}}]}],"attrs":{}}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"是否存在時間複雜度過高的算法?是否存在大量重複計算?","attrs":{}}]}],"attrs":{}}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"是否重複初始化相同的對象?是否存在內存泄露?","attrs":{}}]}],"attrs":{}}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"接口速度問題","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在列表查詢等依賴後臺數據展示的頁面,接口的響應速度也至關重要。由於我們通過 Node.js 搭建的 BFF 來整合多個服務提供方的接口,因此可能存在以下幾個問題:","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"後端服務提供的接口速度是否響應慢?網關、數據庫、索引等服務是否正常?","attrs":{}}]}],"attrs":{}}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"針對實時性要求較低的數據,是否可以利用緩存服務?","attrs":{}}]}],"attrs":{}}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"同時調用多方接口時,是否最大化進行併發請求?非必要接口是否可以單獨發起請求?","attrs":{}}]}],"attrs":{}}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"與瀏覽器腳本一樣,是否存在複雜算法、內存泄露等問題代碼?","attrs":{}}]}],"attrs":{}}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"解決方案","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"帶着以上的這些問題,我們開始着手對現有的應用進行一次詳細的檢查,逐步定位影響性能的關鍵問題並一一進行解決。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"資源加載優化","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"Webpack 構建問題分析","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由於我們的項目通過 Webpack 4.x 構建,因此爲了分析資源文件的個數及大小,採用了 Webpack 插件webpack-bundle-analyzer對產出的靜態資源文件進行了統計,如下圖所示(截取了幾個體積較大的文件)。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/97/977f038ad56ee450b4a2756543cc1339.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"根據統計我們發現了以下幾個主要問題:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"緩存問題。每次改動任意代碼,所有生成的 JS/CSS 等文件的 Hash 值都發生了變化,這意味着每次發佈上線,瀏覽器都需要重新請求全部資源。","attrs":{}}]}],"attrs":{}}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"文件大小。通過 node_modules 生成的 chunk-vendor 原始大小超過 1.5 M。其中,體積最大的是 ElementUI,超過 650K,其次是 moment.js,體積超過 250K。剩餘部分則由 Vue.js、Lodash 等基礎類庫組成。","attrs":{}}]}],"attrs":{}}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"重複打包。部分業務模塊對應的 chunk 文件原始大小在 500K 左右。其原因是使用到了 d3,echarts 等依賴的模塊,直接將它們打包到了對應模塊中。而這些第三方庫,佔整個文件大小的 70% 左右。","attrs":{}}]}],"attrs":{}}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"資源個數。由 Webpack 自動生成了多個模塊間的公共 chunk,大小在幾 K 到一百多 K 不等。例如有三個模塊 a,b,c,則自動生成的 chunk 包含多種不同的組合 a~b.js,a~c.js,a~b~c.js,請求 a 模塊的時候也會同步加載這幾個文件。隨着模塊數量增加,組合也更復雜,無形中也增加了請求的數量。","attrs":{}}]}],"attrs":{}}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"瀏覽器加載速度分析","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通過瀏覽器 Network 工具,我們發現服務器緩存、網絡傳輸等對加載速度影響很小,導致慢的幾個主要問題如下:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"併發數量。通過構建得到的靜態資源文件都部署到一個靜態域名下面,導致需要排隊下載文件。","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"順序問題。一些非首次渲染所需要的 JS 文件(如播放器 SDK 、流程圖 SDK 等)在頁面打開的時候就進行了阻塞加載。","attrs":{}}]}],"attrs":{}}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"資源構建及部署優化方案","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"針對以上問題,我們對 Webpack 配置方式做了以下幾點改進。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"單獨部署基礎庫至 CDN。生產環境將 Vue.js + VueRouter + Vuex + VueCompositionAPI + ElementUI + Lodash 等基礎類庫通過 webpack.DllPlugin 提前構建爲 library.dll.js 並單獨部署,同時整個站點中通過 prefetch 提前加載。","attrs":{}}]}],"attrs":{}}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"單獨部署樣式主題至 CDN。項目中用到的 ElementUI 組件樣式及團隊內部開發的 MaterialTheme 主題樣式放棄從 NPM 引入 Sass 源碼。而是提前構建好 9 種不同顏色的主題,提前部署至 CDN,並通過 prefetch 提前加載。項目中的自定義樣式則通過 Sass Mixin 生成不同主題的規則。","attrs":{}}]}],"attrs":{}}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"將業務代碼部署至與基礎庫不同的域名。提升瀏覽器併發請求的數量。","attrs":{}}]}],"attrs":{}}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"將播放器 SDK、流程圖 SDK 等非首次渲染必須的 JS 文件通過 defer 等方式進行異步加載,或改爲組件初始化時動態請求。","attrs":{}}]}],"attrs":{}}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"刪除 moment.js 等非必須的第三方類庫。通過查看項目源碼,發現僅幾個地方用到了 moment.js 的格式化功能,因此我們選擇通過自己實現一個僅幾十行的工具函數來替換。此外根據項目實際情況,也可以考慮在項目中引入體積更小的類庫,例如 Day.js 等。","attrs":{}}]}],"attrs":{}}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"優化 Webpack 的 splitChunks 策略。將 d3,echarts 等依賴抽取爲單獨的 chunk。此外,考慮到不同模塊之間自動生成的公共 chunk(類似 a~b~c.js)文件不大,反而增加了請求數量,因此禁用了該項配置。同時,顯示地將各模塊間公共的部分(項目中統一放在 src/common 目錄下)打包至 chunk-common 文件中。","attrs":{}}]}],"attrs":{}}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"// webpack config\n{\n optimization: {\n splitChunks: {\n cacheGroups: {\n // 禁用默認拆分的 chunk\n default: false,\n // 顯示抽取項目公共 chunk\n common: {\n name: 'chunk-common',\n test: /src[\\\\/]common/,\n chunks: 'all'\n },\n // 抽取 d3/echarts 等第三方類庫\n d3: {\n name: 'chunk-d3',\n test: /[\\\\/]node_modules[\\\\/](d3|dagre|graphlib)/,\n priority: 100,\n chunks: 'all'\n },\n echarts: {\n name: 'chunk-echarts',\n test: /[\\\\/]node_modules[\\\\/](echarts|zrender)/,\n priority: 110,\n chunks: 'all'\n }\n }\n }\n },\n}","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"優化構建後文件名中的Hash。在生產環境改用 contenthash 來命名文件,僅當包含的文件內容發生改變時纔會重新生成新的文件名,最大化利用緩存。","attrs":{}}]}],"attrs":{}}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"// webpack config\n{\n output: {\n filename: 'js/[name].[contenthash].js',\n chunkFilename: 'js/[name].[contenthash].js'\n }\n}","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"經過以上優化,最終構建的 chunk-vendor 大小在 500K 左右,體積大約減小 2/3;新抽取的項目公共文件 chunk-common 大小 300K 左右;各個模塊打包的文件大小則在 200K 左右, 體積大約減小 3/5。同時,結合 CDN 部署基礎類庫,prefetch 預加載及 contenthash 緩存控制等,資源加載的速度大幅度提升。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"頁面渲染優化","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"考慮到業務場景及開發成本,新版本的前端應用並沒有實現服務器端渲染,存在着較長的白屏時間。而老版本則通過 Java + Velocity 在服務器端完成渲染,兩相對比,用戶體驗相差甚多。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"瀏覽器渲染性能分析","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲了解決這個問題,我們通過 Chrome Performance 對頁面的渲染性能進行了完整的分析。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由於生產環境代碼已經壓縮,這裏建議在開發環境錄製 Profile,可以直接定位到相關源碼。錄製後的時間線展示參見下面截圖。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/d7/d7aad2b013d535a47e4e5c0f248ef36e.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其中我們需要重點關注的幾個維度如下:","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Frames:渲染的 FPS 以及不同時間點的渲染結果。","attrs":{}}]}],"attrs":{}}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Main:渲染主線程,包括 HTML 解析,JavaScript 執行等任務。","attrs":{}}]}],"attrs":{}}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Timings:包括 FP、DCL、FCP、LCP 等指標,以及通過 Performance API 記錄的運行時間。Vue.js 2.x 中可以通過 Vue.config.performance = true; 開啓組件性能記錄。下圖的截圖展示了 Vue.js 組件的渲染耗時情況。","attrs":{}}]}],"attrs":{}}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/c9/c9afd7ad1e0dc1e0fc779c3c69ed906f.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"經過分析,我們發現以下幾個主要問題:","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"路由激活後的首次渲染任務耗時特別長,已經超過了 2 秒。其中,站點導航、側邊欄等就佔用了一半以上的時間。","attrs":{}}]}],"attrs":{}}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"導航組件中,用於判斷鏈接權限的 AuthService.hasURIAuth 方法佔用了 80% 的時間。","attrs":{}}]}],"attrs":{}}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在通過配置渲染的動態表單頁中,核心組件 FormBuilder 渲染時間也在 2 秒左右。","attrs":{}}]}],"attrs":{}}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"頁面渲染整體優化方案","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"針對以上問題,我們進行了以下幾點改進:","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通過服務器端渲染骨架屏,包括導航等頁面基礎佈局。從視覺效果上減少用戶的心理等待。","attrs":{}}]}],"attrs":{}}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"減少首屏渲染的組件數量。將初始爲隱藏狀態的導航二級菜單、站點側邊欄、列表高級搜索彈窗等組件通過 webpack 提取至異步 chunk 中,在用戶交互時再異步渲染。","attrs":{}}]}],"attrs":{}}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"// AppLayout.vue\n{\n components: {\n AppDrawer: () =>\n import(\n /* webpackChunkName: 'chunk-async-common' */\n './AppDrawer'\n ),\n AppHeader\n },\n}","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"將根據配置進行渲染的動態表單 FormBuilder 手動拆分爲多個渲染任務。由於業務場景的複雜性,通常一個表單擁有 80 餘個字段。而在 Vue.js 裏面,一次數據變化觸發的渲染任務是無法直接拆分的。這裏我們採取了另一種方式,將表單配置拆分爲多段,首次渲染時僅傳遞第一段配置,然後在後續的渲染週期依次將配置拼接上去。","attrs":{}}]}],"attrs":{}}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"{\n created() {\n this.getFormConfig().then(() => {\n this.startWork();\n });\n },\n methods: {\n startWork() {\n const work = () => {\n // 任務調度器\n return scheduler.next(() => {\n // 逐步拼接表單配置\n this.formConfig = this.concatNextFormConfig();\n\n if (!scheduler.done()) {\n // 循環執行任務\n work();\n }\n });\n };\n \n // 啓動首次任務\n work();\n }\n }\n}","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"接口速度優化","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"BFF 性能分析","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由於業務流程複雜,前端會調用多個服務接口,並對數據進行二次處理,因此一直由前端來負責Java Web層(BFF)的開發。本次升級爲了開發更簡便,引入了基於TypeScript的NestJS框架替換原來Spring MVC,由NestJS封裝面向前端的接口給 VueJS應用。爲了定位其中潛在的性能問題,我們做了一些通用的擴展:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲所有封裝的接口添加自定義中間件 TimeMiddleware,用於統計接口的整體響應速度。","attrs":{}}]}],"attrs":{}}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲 axios 統一添加 interceptor,用於統計 BFF 調用第三方接口的響應速度。","attrs":{}}]}],"attrs":{}}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最後,通過日誌、Apache JMeter 等工具對核心接口進行分析,我們主要發現以下幾個問題:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在同時調用多方服務的接口 B 中,存在不必要的串行。此外,其中一個標籤查詢服務平均耗時在 700ms 左右,成爲影響速度的關鍵因素。","attrs":{}}]}],"attrs":{}}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在獲取用戶信息的接口 C 中,有 20% 左右的請求耗時在 600ms 左右,而其他的請求僅耗時 50ms。經過定位發現是服務集羣中某臺服務器跨地區導致。","attrs":{}}]}],"attrs":{}}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"大部分接口都依賴了一個獲取頻道列表的基礎服務,實時性要求很低,然而每次都是通過接口實時獲取,耗時大約 50 ms。","attrs":{}}]}],"attrs":{}}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"整個應用的日誌服務繼承了 NestJS 的 logger.service ,它默認是通過 process.stdout 同步輸出的。因此日誌內容較多時在部分機器上開銷也很大,平均耗時 100ms 左右。","attrs":{}}]}],"attrs":{}}],"attrs":{}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"BFF 整體優化方案","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"針對以上問題,我們進行了以下幾點改進:","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"後端同學優化 ES 查詢服務,新增多臺物理機進行擴容。優化後平均耗時小於 1 秒,速度提升超過 60%。","attrs":{}}]}],"attrs":{}}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"後端同學爲標籤查詢服務添加緩存機制,優化後平均耗時 200ms 左右,整體提升超過 70%。","attrs":{}}]}],"attrs":{}}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"移除集羣中的跨地區服務器,保證各服務之間儘量在同一個地區、機房。","attrs":{}}]}],"attrs":{}}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"大化地並行請求,減少請求耗時的關鍵路徑。以其中一個接口爲例,優化前平均耗時 1.3 秒,優化後平均耗時僅 700ms,提升 45% 左右。","attrs":{}}]}],"attrs":{}}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"實時性要求較低的服務通過 Redis 緩存查詢結果,例如頻道查詢服務,平均耗時從 50ms 減少至 15ms,提升 70% 左右。","attrs":{}}]}],"attrs":{}}],"attrs":{}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"優化後整體效果展示","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"資源加載速度展示","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通過減少文件大小及個數、緩存、併發、預加載、懶加載等各種優化,獲取核心資源整體耗時控制在 200ms 左右。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/b0/b0d41fac922d40a9117680bfb1742302.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/89/89e1bca1b3a5ffaddaa8ea87e55b07ee.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"頁面渲染速度展示","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通過異步渲染隱藏組件、優化耗時函數、任務分片、骨架屏等方式,讓用戶儘早看到內容的同時,將首次路由渲染的時間控制在 1 秒以內,結合瀏覽器自身的優化,在電腦網速及性能正常的情況下,已經感知不到白屏的存在。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/8f/8f3c3ae3f14d52cff75d59b831e21747.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"接口相應速度展示","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通過擴容、緩存、併發、優化耗時函數等方式,我們將核心的幾個查詢接口的速度也控制在了 1 秒左右。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/ec/ece1c0dd81d7bc3cb9da41747ae6ac89.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"優化前後核心數據對比","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/25/2521b108488d1fb600af52ba0be8a4c2.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"後記","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"前端的性能優化涉及到方方面面,每一個環節其實都有優化的空間。這次實踐,我們針對項目的實際場景,主要從資源加載、渲染性能和接口速度三個方面來分析並解決問題,一步一步提升頁面的打開速度,也爲用戶帶來了更好的使用體驗。當然,優化無止境,希望本文能起到拋磚引玉的作用,感興趣的同學可以留言討論。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原文鏈接:","attrs":{}},{"type":"link","attrs":{"href":"https://mp.weixin.qq.com/s/H88469QNdE-IChc8u-pBaQ","title":""},"content":[{"type":"text","text":"優化無止境,愛奇藝中後臺 Web 應用性能優化實踐","attrs":{}}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章