驗證碼前端效能分析及優化實踐
點選上方藍字關注騰訊防水牆
瞭解第一手企業安全資訊
/ / / / / / / / / / /
在越來越注重使用者體驗的趨勢下,驗證碼作為一種自打誕生以來就被貼上“多餘”標籤的產品,更應該給使用者提供良好的體驗。本文以滑動驗證碼作為切入點,分析了驗證碼可能存在的效能問題,並通過資源合併、DOM操作優化和選擇性內聯打包等方式有效減少頁面載入時間。同時本文總結了移動端元件化適配容易遇到的問題,並提供了規範化的解決方案。希望本文能給前端做效能優化的同學提供一些不一樣的實踐思路。
1. 驗證碼現狀
優化前,我們優先對現有驗證碼進行了分析,總結其存在的主要問題。
-
資源亂象
-
無節制的DOM及非同步等待
-
移動端適配不規範
資源亂象
資源亂象是老舊工程普遍存在的問題,作為jQuery時代遺留下來的驗證碼工程,該現象同樣存在。表1.1展現了舊版驗證碼資原始檔情況,從表中可以看出,舊版驗證碼資源使用相對混亂,尤其是js/css資源,分散的多個小檔案大大增加了HTTP請求數量。驗證碼的圖片資源都是一些小圖示,雖然前期的一些優化已經將這些小圖示合併成雪碧圖,但對於少量的小圖示仍然有優化空間。
表1.1 驗證碼前端資原始檔分佈
無節制的DOM及非同步等待
驗證碼擁有諸多定製化能力,包括彈出形式、多語言、主題色、頭部和反饋按鈕等,舊版驗證碼為了實現定製化能力進行了大量冗餘的DOM操作,不僅消耗效能,而且載入過程會出現介面閃爍。同時舊版驗證碼為了解決一些非同步等待的問題,引入了諸多定時器,看似無腦,卻大大增加了邏輯複雜度,使得程式碼難以維護。
移動端適配不規範
舊版驗證碼未進行體系化的移動端適配,所有適配的樣式均為js實時計算完成後寫入DOM的style中,邏輯複雜。此前收到諸多使用者反饋均與此適配方式有關,前端經常需要為了適配某一種機型而堆疊程式碼,苦不堪言。
面臨著如此多的歷史包袱,重構勢在必行。
2. 方案及技術選型
圖2.1是驗證碼的前端框架,驗證碼的主要頁面使用iframe載入,iframe隔離了業務頁面,為驗證碼提供了更可控更乾淨的執行環境。業務頁面接入時需要引入一個驗證碼的入口js,該js負責分配不同型別的驗證碼、建立iframe,並管理iframe與業務頁面的通訊。
圖2.1 驗證碼前端框架
任何完全推翻原有架構的重構都是耍流氓。驗證碼重構的原則是保持原有的業務邏輯,只重構前端實現。因此本次重構將圍繞綠色iframe部分進行,其他部分保持不變,這樣既能保證業務平滑過渡,也能讓原有測試樣例完整覆蓋。
一個專案能否具備良好的可維護性,前期技術選型尤為重要。重構前,基於對驗證碼整體功能的評估,最終將整體優化方案劃分為3個部分。本文也將圍繞這3個部分,講述驗證碼前端效能優化的思考及方案。
-
資源合併
-
模組化
-
DOM操作優化
-
資源合併打包
-
圖片、樣式內聯
-
規範移動端適配
-
引入flexible.js
-
rem自動換算
-
iframe內縮放問題
-
webview內適配問題
-
提升使用者預期
3. 資源合併
資源合併是一個很大的方向,不同的產品合併方式各不相同。如圖3.1所示,是整個驗證碼的載入流程,綠框內的節點為本次優化需要關注的階段。
圖3.1 驗證碼載入流程
重構前我們對各階段耗時進行了分析,其中DOM渲染和驗證碼主邏輯耗時佔比相對較高,屬於需要重點優化的部分。而驗證碼型別請求、iframe建立、iframe請求和圖片載入與瀏覽器效能及服務端響應相關,因此暫時不進行優化,後期考慮預載入。
圖3.2 驗證碼載入時間
3.1 模組化
從載入流程可以看出,驗證碼並不是一個簡單的幾十上百行程式碼就能實現的工程,複雜的邏輯需要分模組才能更好地管理團隊分工和整體進度,模組化也有利於單元測試,保證工程的質量。然而,JavaScript/">JavaScript並不是一種模組化程式語言,其模組化需要依賴額外的工具實現。早期的模組化有多種不同的寫法,想要詳細瞭解的讀者可閱讀阮一峰老師的《Javascript模組化程式設計(一):模組的寫法》。
模組化發展的中期,require.js和sea.js佔據了模組化解決方案的半壁江山,這兩個方案都是對模組化很好的實踐。驗證碼將採用Node.js模組系統的CommonJS規範,藉助webpack4將模組打包到一起。最終驗證碼工程中模組引用及匯出相關的程式碼非常簡單:
const module1 = require('./module1');
// ...
function Captcha() {}
// ...
module.exports = Captcha;
模組化是資源合併的基礎,這裡的模組化不僅是將業務邏輯分模組載入,也包含了CSS、Icon等樣式和圖示。只有把所有資源均納入到模組化管理中,配合webpack不同的loader,才能更細粒度地控制合併資源。
3.2 DOM操作優化
DOM操作是整個JavaScript執行週期中最耗時的操作之一,不合理的DOM操作不僅會觸發頁面大量的重排和重繪,還有可能導致頁面假死。良好的DOM組織結構應該遵循不需要大量重排(Reflow),DOM修改儘量用重繪(Repaint)代替的原則。
圖3.3 重繪&重排
重繪是指一個元素的外觀被改變,但佈局沒有發生變化,包括改變visibility、outline、background等屬性;而重排是DOM的變化影響了元素的幾何屬性,瀏覽器會重新計算元素的幾何屬性,使渲染樹中受到影響的部分失效,會驗證DOM樹上的所有其它節點的visibility屬性,十分低效,這類操作包括改變視窗大小、文字大小、內容,style屬性等。
本次重構針對舊版工程大量不合理的DOM操作進行優化,採取的解決方案包括:
-
最小化重繪和重排
重新合理組織了DOM結構,將DOM的多樣化用CSS類的方式表示,儘量控制DOM的顯示或隱藏,避免新增或刪除,對於一些要修改的DOM結構,採用修改className的方式,而不是多次讀寫style,避免使用js修復佈局,減小DOM操作對頁面渲染造成的影響。
-
快取DOM元素
如果某個節點將在後續進行多次操作,則將其利用變數儲存起來,而不是每次進行操作時都去查詢一遍該節點。
-
消滅定時器
對於一些動畫效果的實現,避免使用setTimeout或setInterval,而是用requestAnimationFrame代替
3.3 資源合併打包
js指令碼的載入和執行會阻塞HTML Parser,本次優化本著"all in one"的原則,運用元件化思想,將複雜的邏輯抽象成多個獨立的模組,把前端程式碼拆分成多個小檔案,利用webpack靈活地處理模組依賴、提取公共模組最終合成業務所需程式碼。通過webpack-bundle-analyzer外掛將打包後的內容束展示為方便互動的直觀樹狀圖,如圖3.4所示:
圖3.4 驗證碼專案結構
滑動驗證碼支援20多個國家的23種語言,文案數多達30餘條,因此語言包的體積佔了整個工程的很大比例。Zepto有很多模組,通常為了使引用的zepto庫儘可能小,會根據專案的實際需要打包所需的模組進去,驗證碼專案只需用到事件處理、網路請求和一些簡單的動畫效果,因此我們對zepto、event、ajax和fx(動畫模組)四個模組進行了打包處理,生成本專案特有的zepto庫。整個專案結構中佔用體積較大的js來自多語言包和zepto庫,其餘都是體積可忽略不計的小元件。
通過webpack對js的模組化管理,整個專案的模組結構變得十分清晰,方便維護和重構。Js資原始檔的數量由10餘個減小為1個,體積也得到了很好的控制,減少了HTTP請求數量並且沒有增加頻寬消耗。
3.4 圖片、樣式內聯
驗證碼中的圖片均為Icon,特點是體積小但數量較多,合併成雪碧圖後還是需要佔用幾十KB的空間,並且載入雪碧圖需要一次額外的HTTP請求。針對驗證碼小圖示眾多這一特性,我們將這些小圖示base64編碼到css檔案中,減少HTTP請求數量,並且並不會佔用很大的空間。
test: /\.(png|jpg|gif)$/, use: [{ loader: 'url-loader', options: { limit: 8192 } }]
在工程中新增上述webpack配置,採用url-loader外掛自動對小於8KB的小圖示進行DataURL處理,直接把圖片資源內嵌到頁面中提供給瀏覽器解析,這樣就可以不用傳送HTTP請求,提升資原始檔載入速度的同時也節省了頻寬資源。
驗證碼的DOM結構並不算太複雜,並且經過對SCSS的模組化處理,使用PostCSS生態和cssnext來支援自定義變數、樣式內嵌等特性,最終編譯生成的CSS檔案已經很簡潔,體積只有十幾KB,因此我們考慮不再將其作為一個單獨的資原始檔引用,而是直接將其inline到html中,在html的head標籤中新增如下style配置:
<style type="text/css"><%= compilation.assets[htmlWebpackPlugin.files.css[0].substr(htmlWebpackPlugin.files.publicPath.length)].source() %></style>
webpack的compilation物件繼承於compiler,能獲取一切編譯後的內容,因此可以直接把打包生成的css樣式插入html,減少了額外的HTTP請求,並且能避免偶發的CDN資源載入失敗導致的頁面顯示異常。
4. 移動端適配
4.1 引入flexible.js
flexible.js是一個開源的用於終端裝置的適配解決方案,主要用於解決各種不同尺寸移動裝置的大小自適應問題,其原理是通過移動裝置的dpr(裝置畫素比=物理畫素/裝置獨立畫素)和螢幕寬度來動態改變html的font-size大小。因此我們選擇flexible.js用於驗證碼的頁面適配,在頁面引入flexible.js後,首先獲取裝置型號,然後根據不同裝置在標籤上新增一個data-dpr和font-size樣式,並結合我們的專案對其進行改進,獲得更加完美的相容效果。
4.2 rem自動換算
下面是驗證碼頁面的縮放配置,其中,baseDpr表示基準的dpr值為1,rem單位以375寬度的螢幕為基準,即1rem==37.5px,並提供'!px'和'!no'兩種特殊的單位轉換方式。
// px2rem const px2remConfigs = { baseDpr: 1, remUnit: 37.5, forcePxComment: '!px', keepComment: '!no' };
通過postcss-loader的px2rem外掛將原生scss檔案中所有用px單位表示的大小自動編譯轉換成能夠自適應於各螢幕大小的rem值。同時,會對px後面帶'!px'和'!no'字尾的情況做特殊處理,在px後面新增'!px'表示根據dpr設定成不同的rem值,一般字型用該單位修飾;在px後面新增'!no'表示不轉化px直接原樣輸出,一般boder邊框用該單位修飾。
loader: "postcss-loader", options: { plugins: [ autoprefixer, // 自動新增字首 px2rem(px2remConfigs) ] }
針對一些元素尺寸、位置等需要動態變化的情況,比如橫豎屏切換時iframe整體大小需要自適應,再比如每重新整理一次,小拼圖的寬、高、top值都要重新計算,此時只需要註冊相應的回撥函式,在回撥函式內進行相應的邏輯處理即可。flexible檢測到resize、pageshow或其它呼叫refreshRem方法的時候,會回撥在驗證碼內註冊的回撥陣列(resizeCb)中的所有方法。
listen: function(order, cb) { if(flexible.resizeCb) { flexible.resizeCb.splice(order, 0, cb); } } flexible.listen(0, function() {...}; flexible.listen(1, function() {...}; flexible.listen(2, function() {...}; ... if(flexible.resizeCb) { for(var i=0; i < flexible.resizeCb.length; i++) { try{ flexible.resizeCb[i] && flexible.resizeCb[i](); } catch(e) {} } }
4.3 iframe內縮放問題
驗證碼作為一個web元件提供給業務使用,在iframe內部預設不設定視口(viewport),在dpr大於1的時候整個iframe會被壓縮成1/dpr,如圖4.1左側所示。因此需要根據業務頁面設定的viewport來設定縮放比例,通過獲取父頁面的scale值並將其透傳到iframe內部的方式來設定正確的縮放比例,最終得到如圖4.1右側所示的效果。
圖4.1 iframe內縮放問題
4.4 webview內適配問題
雖然flexible能比較完美地適配移動端頁面,然而在一些特殊的安卓機器中仍然會存在很詭異的適配問題,如圖4.2所示:
圖4.2 webview內適配問題
產生這種情況的原因是安卓部分webview修改了預設字型,使得最終顯示的1rem的px值和設定的值不一致,導致頁面顯示異常。正常情況下,rem是根據html的最終font-size進行響應的,並且對於大部分機型,設定的值和最終的響應值是相等的,即:
1rem == finalDocElementFontSize == docElementFontSize
然而在小米MAX和榮耀8等機型中,最終的響應值要大於設定的值,導致以rem為單位的DOM元素都顯示過大,就會出現圖4.2中小拼圖缺口大小不匹配、圖片超出螢幕區域的異常情況。
表4.1 優化前後採集資料對比
如表4.1所示,其中,優化前所在行是我們採集上來的異常資料,可以明顯看到,真實的1rem對應的px值要遠大於設定的值,此時設定一個閾值,當設定值和最終響應值差值大於0.5時,重新計算fontsize大小:
try { var finalDocElementFontSize = parseFloat(getComputedStyle(docEl).fontSize); if(Math.abs(finalDocElementFontSize - docElementFontSize) > 0.5) { var delta = finalDocElementFontSize / docElementFontSize; docEl.style.fontSize = (docElementFontSize / delta) + 'px'; } } catch(e) {}
得到矯正後的優化後資料,使得最終的響應值接近我們最初想要設定的值,頁面就能達到如圖4.3所示完美的適配效果:
圖4.3 優化後適配效果
5. 提升使用者預期
圖5.1是舊版驗證碼的載入流程,白屏時間接近2s,並且整個過程銜接得並不自然。
圖5.1 舊驗證碼載入流程
因此我們在重構中引入了Skeleton Screen(載入佔位圖),在驗證碼載入預期填充灰色的佔位圖,實現介面載入過程中的平滑過渡效果。這個概念來源於iOS設計規範中的Lanuch Screen(啟動螢幕),主要目的是為了解決等待載入過程中出現白屏或介面閃爍造成的割裂感。
圖5.2 骨架屏
實現的效果如圖5.2所示,用佔位圖展示驗證碼的大致骨架,大大提升使用者對驗證碼載入的預期。最終載入流程如圖5.3所示。
圖5.3 新驗證碼載入流程
載入佔位圖的顯示不依賴任何頁面外部資源,在驗證碼的HTML載入完成之前就可以顯示出驗證碼的大致輪廓,增加使用者的等待預期並減少長時間白屏帶來的焦躁情緒,使用者體驗得以提升。
6. 優化效果及總結
經過本輪重構以後,驗證碼整體效能得到了大幅提升。其中,驗證碼體積減少了38%,http資源請求數由10餘個減少為2個。引入骨架屏有效縮短了白屏時間,最耗時的DOM渲染和驗證碼主邏輯執行時間也分別減少了50%和60%。安卓下全流程載入平均耗時從3.9s減少為1.9s,降低了51%;ios下從3s減少為1.7s,降低了43%。
本次移動端驗證碼重構是對前端效能優化的一次完整實踐,模組化、資源合併打包、按需快取、程式碼壓縮等前端的優化思路,在基礎前端產品中應用後效果尤為顯著,優化後的驗證碼很大程度上減少了網路鏈路的開銷。並且重構過程中梳理了驗證碼的載入流程,刪減了流程中不合理的非同步等待,最終的主邏輯程式碼中已經沒有定時器的影子,這再一次證明了前端通用優化思路的可行性。
一個專案存在歷史包袱在所難免,隨著各種新技術層出不窮,一些老舊的思想和方式勢必被更好的實踐替代,效能優化是值得前端從事者深入研究的領域,只有不斷實踐通用優化方法和探索最佳實踐標準,才能提升使用者體驗從而為業務創造價值。為了使用者體驗,我們一直在努力。
我們將對各類技術進行持續的 探索與 研究,為業務提供優質的安全解決方案以及威脅情報感知服務。如果對相關技術及業務安全感興趣, 歡迎訪問007.qq.com,或者關注 微信公眾號 瞭解更多。