日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 前端技术 > HTML >内容正文

HTML

前端如何做极致的首屏渲染速度优化

發布時間:2024/7/5 HTML 37 豆豆
生活随笔 收集整理的這篇文章主要介紹了 前端如何做极致的首屏渲染速度优化 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

這里說的極致是技術上可以達到最優的性能。

這里不討論常見的優化手段,比如:Script標簽放到底部、DNS預解析、HTTP2.0、CDN、資源壓縮、懶加載等。

這里討論的是如何使First Contentful Paint的時間降到最低,這個指標決定了白屏的時間有多長。

在正式開始之前,我們以LCG(Vue組件代碼生成平臺來說),它的FCP(First Contentful Paint)速度在Slow 3G情況下在將近40s左右:

這顯然是一個讓人無法忍受的時間。

常規情況下,我們為了縮短First Contentful Paint的時間,可以在index.html中內聯一個Loading效果。

但拿大型項目來說,尤其是以VueCli創建的項目來說,這個Loading的效果不見得能有多提前,因為大型項目中所依賴的資源非常多。所以說能做到極致并不容易。

問題出在哪?默認Vue-Cli會在生成的文件頭部增加很多的link,而這些link會阻礙后面靜態Html內容的處理,等這些靜態Html內容處理完才會有Dom的生成以及動畫的執行。

假設我們最終輸出的index.html文件內部是這樣的:

那我們的loading效果顯然不會出現的有多早。所以,我們的極致目標就是讓loading動畫盡可能的早。

為了看出優化前優化后的效果差異,一切都在瀏覽器的Slow 3G網絡情況下驗證。

有Loading情況下優化前后效果數據比對

下面的圖展示了單純的在index.html頂部增加loading.css文件的效果,這個時間從40秒縮短到了22秒左右,效果是要好一些了,但是還是讓人無法忍受:

而優化后可以將時間縮短到2.4秒不到,注意這是在Slow 3G網絡情況下測試的結果,且網絡傳輸速度花費了2.14秒

這個時間是要比百度還要好一些的:

那究竟是怎么做到的呢?

思路

我們可以從第二張圖中看到,FCP很明顯是在babel.min.js文件加載之后才開始進行的。而我們理想中的時間應該在4秒多一些。顯然,是一些JS文件的加載阻礙了DOM的解析。

但真的只有JS文件對loading有影響嗎?其它類型的,比如PNG、SVG、CSS、JSON會影響Loading的渲染速度嗎?

會,FCP會等待所有的CSS加載完成才開始進行,而css文件的加載優先級默認是最高的。如果script標簽擁有rel="preload"并且書寫在css之前則會比css優先加載(這里的正確性有待驗證),資源的加載默認情況下是按照書寫順序進行的。更具體的內容可以查看末尾的延伸閱讀

所以我們可以試著將所有的link放置到body的最后面。

怎么做

因為使用的是VueCli(4.5.9版),因此我們可用的HtmlWebpackPlugin的版本只有3.2.0。而這個版本是在3年前發布的,所以只能對這個版本現有的能力動一下刀子。文檔:html-webpack-plugin 3.2.0。

在文檔中查到這個版本其實是支持一些事件鉤子的:

  • html-webpack-plugin-before-html-generation
  • html-webpack-plugin-before-html-processing
  • html-webpack-plugin-alter-asset-tags
  • html-webpack-plugin-after-html-processing
  • html-webpack-plugin-after-emit

文檔下方有個簡單的例子演示了這些鉤子怎么使用,但實際發現時,它這里的例子是有些問題的,因為cb是一個undefined:

function MyPlugin(options) {// Configure your plugin with options... }MyPlugin.prototype.apply = function (compiler) {compiler.plugin('compilation', (compilation) => {console.log('The compiler is starting a new compilation...');compilation.plugin('html-webpack-plugin-before-html-processing',(data, cb) => {data.html += 'The Magic Footer'cb(null, data)})}) }module.exports = MyPlugin

不過這些難不倒我,通過調試時的堆棧得知,我所使用的html-webpack-plugin在回調自定義方法時是同步進行的,所以只需要將data return就可以了。

經過這樣的方式一步步調試,最終知道了html-webpackp-plugin是怎么生成html代碼的:

injectAssetsIntoHtml (html, assets, assetTags) {const htmlRegExp = /(<html[^>]*>)/i;const headRegExp = /(<\/head\s*>)/i;const bodyRegExp = /(<\/body\s*>)/i;const body = assetTags.body.map(this.createHtmlTag.bind(this));const head = assetTags.head.map(this.createHtmlTag.bind(this));if (body.length) {if (bodyRegExp.test(html)) {// Append assets to body elementhtml = html.replace(bodyRegExp, match => body.join('') + match);} else {// Append scripts to the end of the file if no <body> element exists:html += body.join('');}}// 這里就是我要找的關鍵部分if (head.length) {// Create a head tag if none existsif (!headRegExp.test(html)) {if (!htmlRegExp.test(html)) {html = '<head></head>' + html;} else {html = html.replace(htmlRegExp, match => match + '<head></head>');}}// Append assets to head elementhtml = html.replace(headRegExp, match => head.join('') + match);}// Inject manifest into the opening html tagif (assets.manifest) {html = html.replace(/(<html[^>]*)(>)/i, (match, start, end) => {// Append the manifest only if no manifest was specifiedif (/\smanifest\s*=/.test(match)) {return match;}return start + ' manifest="' + assets.manifest + '"' + end;});}return html;}

那么知道了它是怎么做的,但它沒有提供對外的方法來干擾這些head要放到什么位置。比如我現在就想把他們放到body最后面,但它是不支持的。

那么我初步的想法是在html生成后將那部分的head手動轉移一下。但突發奇想,既然有鉤子可以更改AssetTags,那我豈不是可以不讓它內部生成而讓我自己生成?這個想法很妙。經過一番調試得知,可以在html-webpack-plugin-alter-asset-tags這個鉤子中拿到data.head的內容,再將data.head給置空數組。這樣它原本的head就不會生成了。這里的head代表的就是即將插到head中的那些標簽。

然后再在html-webpack-plugin-after-html-processing這個鉤子中按照html-wepack-plugin的方式給拼接到body的最后面。

于是有了最終代碼:

// AlterPlugin.js function AlterPlugin(options) { }function createHtmlTag(tagDefinition) {const attributes = Object.keys(tagDefinition.attributes || {}).filter(attributeName => tagDefinition.attributes[attributeName] !== false).map(attributeName => {if (tagDefinition.attributes[attributeName] === true) {return attributeName;}return attributeName + '="' + tagDefinition.attributes[attributeName] + '"';});const voidTag = tagDefinition.voidTag !== undefined ? tagDefinition.voidTag : !tagDefinition.closeTag;const selfClosingTag = tagDefinition.voidTag !== undefined ? tagDefinition.voidTag : tagDefinition.selfClosingTag;return '<' + [tagDefinition.tagName].concat(attributes).join(' ') + (selfClosingTag ? '/' : '') + '>' +(tagDefinition.innerHTML || '') +(voidTag ? '' : '</' + tagDefinition.tagName + '>'); }AlterPlugin.prototype.apply = function (compiler) {compiler.plugin('compilation', (compilation) => {let innerHeadTags = null;compilation.plugin('html-webpack-plugin-before-html-generation',(data, cb) => {return data;})compilation.plugin('html-webpack-plugin-before-html-processing',(data, cb) => {return data;})compilation.plugin('html-webpack-plugin-alter-asset-tags',(data, cb) => {// 獲取到它原來的那些headTaginnerHeadTags = data.head.map(createHtmlTag);data.head = [];return data;})compilation.plugin('html-webpack-plugin-after-html-processing',(data, cb) => {// 在這里進行html的內容變更data.html = data.html.replace(/(<\/body\s*>)/i, match => {return innerHeadTags.join('') + match});return data;})compilation.plugin('html-webpack-plugin-after-emit',(data, cb) => {return data;})}) }module.exports = AlterPlugin

最后只需要在vue.config.js中引用一下這個新的Plugin就可以了:

const AlterPlugin = require('./AlterPlugin');module.exports = {...configureWebpack: {plugins: [new AlterPlugin()]},... };

最終的代碼輸出是我想要的結果:

<!DOCTYPE html> <html lang="en"><head><meta charset="utf-8" /><meta http-equiv="X-UA-Compatible" content="IE=edge" /><meta name="viewport" content="width=device-width,initial-scale=1.0" /><link rel="stylesheet" href="loading.css" /> </head><body><div id="app">...</div>...<script defer src="https://cdn.jsdelivr.net/npm/@babel/standalone@7.0.0-beta.42/babel.min.js"></script><!--以下部分都是AlterPlugin作用的結果,這部分結果本來會被放置到head中的--><script type="text/javascript" src="/vue-creater-platform/js/chunk-vendors.js"></script><script type="text/javascript" src="/vue-creater-platform/js/app.js"></script><link href="/vue-creater-platform/js/0.js" rel="prefetch"><link href="/vue-creater-platform/js/1.js" rel="prefetch"><link href="/vue-creater-platform/js/2.js" rel="prefetch"><link href="/vue-creater-platform/js/3.js" rel="prefetch"><link href="/vue-creater-platform/js/about.js" rel="prefetch"><link href="/vue-creater-platform/js/app.js" rel="preload" as="script"><link href="/vue-creater-platform/js/chunk-vendors.js" rel="preload" as="script"><link rel="icon" type="image/png" sizes="32x32" href="/vue-creater-platform/img/icons/favicon-32x32.png"><link rel="icon" type="image/png" sizes="16x16" href="/vue-creater-platform/img/icons/favicon-16x16.png"><link rel="manifest" href="/vue-creater-platform/manifest.json"><meta name="theme-color" content="#4DBA87"><meta name="apple-mobile-web-app-capable" content="no"><meta name="apple-mobile-web-app-status-bar-style" content="default"><meta name="apple-mobile-web-app-title" content="vue-component-creater"><link rel="apple-touch-icon" href="/vue-creater-platform/img/icons/apple-touch-icon-152x152.png"><link rel="mask-icon" href="/vue-creater-platform/img/icons/safari-pinned-tab.svg" color="#4DBA87"><meta name="msapplication-TileImage" content="/vue-creater-platform/img/icons/msapplication-icon-144x144.png"><meta name="msapplication-TileColor" content="#000000"> </body></html>

寫到一半時發現,因為不嚴謹的試驗導致了錯誤的結果,所以這篇文章的產出可以算只有一個可以轉移head標簽的Plugin。

如果把loading.css文件直接內聯效果會不會效果更好?

是可以的,將Loading的樣式直接寫在html中會與上面的一系列操作是同樣的效果。也可以說FCP不需要等待所有的CSS加載完畢再進行。這個結論與文章中有矛盾,還需要驗證First Contentful Paint的具體觸發時機。

*后記
如果要觸發First Contentful Paint,則需要在Dom中至少存在文本或者圖片,否則它是不會被觸發的。原文:
The First Contentful Paint time stamp is when the browser first rendered any text, image (including background images), non-white canvas or SVG. This excludes any content of iframes, but includes text with pending webfonts. This is the first time users could start consuming page content.

延伸閱讀:

  • Paint Timing 1 草案 簡要概述:This document defines an API that can be used to capture a series of key moments (first paint, first contentful paint) during pageload which developers care about.
  • Chrome的First Paint觸發的時機探究 非常詳細
  • User-centric performance metrics

總結

以上是生活随笔為你收集整理的前端如何做极致的首屏渲染速度优化的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。