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

这里说的极致是技术上可以达到最优的性能。

这里不讨论常见的优化手段,比如: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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/478626.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

LeetCode 209. 长度最小的子数组(滑动窗口)

1. 题目 给定一个含有 n 个正整数的数组和一个正整数 s &#xff0c;找出该数组中满足其和 ≥ s 的长度最小的连续子数组。 如果不存在符合条件的连续子数组&#xff0c;返回 0。 示例: 输入: s 7, nums [2,3,1,2,4,3] 输出: 2 解释: 子数组 [4,3] 是该条件下的长度最小的连…

京东 | NLP人才联合培养计划

01 京东AI项目实战课程安排覆盖了从经典的机器学习、文本处理技术、序列模型、深度学习、预训练模型、知识图谱、图神经网络所有必要的技术。项目一、京东健康智能分诊项目第一周&#xff1a;文本处理与特征工程| Bag of Words模型| 从tf-idf到Word2Vec| SkipGram与CBOW| Hiera…

论文小综 | Pre-training on Graphs

本文转载自公众号&#xff1a;浙大KG。作者&#xff1a;方尹、杨海宏&#xff0c;浙江大学在读博士&#xff0c;主要研究方向为图表示学习。在过去几年中&#xff0c;图表示学习和图神经网络(Graph Neural Network, GNN)已成为分析图结构数据的热门研究领域。图表示学习旨在将具…

初探下一代网络隔离与访问控制

概述 安全域隔离是企业安全里最常见而且最基础的话题之一&#xff0c;目前主要的实现方式是网络隔离&#xff08;特别重要的也会在物理上实现隔离&#xff09;。对于很小的公司而言&#xff0c;云上开个VPC就实现了办公网和生产网的基础隔离&#xff0c;但对于有自建的IDC、网络…

LeetCode 189. 旋转数组(环形替换)

1. 题目 给定一个数组&#xff0c;将数组中的元素向右移动 k 个位置&#xff0c;其中 k 是非负数。 示例 1:输入: [1,2,3,4,5,6,7] 和 k 3 输出: [5,6,7,1,2,3,4] 解释: 向右旋转 1 步: [7,1,2,3,4,5,6] 向右旋转 2 步: [6,7,1,2,3,4,5] 向右旋转 3 步: [5,6,7,1,2,3,4]要求…

吴恩达:机器学习应以数据为中心

源 | 新智元今天是吴恩达45岁生日。他是国际最权威的ML学者之一&#xff0c;学生遍布世界各地。在最近的一期线上课程中&#xff0c;吴恩达提出了以模型为中心向以数据为中心的AI。吴恩达发推称&#xff0c;「大家为自己送上最好的礼物就是&#xff0c;观看这个视频观看并提出自…

如何开发小程序开发者工具?

最近集团内部在自研小程序&#xff0c;我负责小程序开发者工具的调试部分。经过一段时间的探索&#xff0c;摸索出不少经过实际检验的可行手段。接下来将会用几篇文章总结一下思路。 文章的内容主要会分为以下几部分&#xff1a; 如何建立逻辑层运行时容器&#xff08;两种方…

美团点评基于Storm的实时数据处理实践

背景 目前美团点评已累计了丰富的线上交易与用户行为数据&#xff0c;为商家赋能需要我们有更强大的专业化数据加工能力&#xff0c;来帮助商家做出正确的决策从而提高用户体验。目前商家端产品在数据应用上主要基于离线数据加工&#xff0c;数据生产调度以“T1”为主&#xff…

会议交流 | 大规模知识图谱的构建及应用分享

本文转载自公众号&#xff1a;DataFunTalk。论坛出品&#xff1a;张伟 阿里巴巴 资深算法专家分享时间&#xff1a;12月19日&#xff0c;9:00-12:00导读&#xff1a;12月19日&#xff0c;9&#xff1a;00-12&#xff1a;00。在DataFunTalk年终大会上&#xff0c;由阿里巴巴资深…

机器翻译小记

神经网络翻译面临的挑战

赛尔原创 | N-LTP:基于预训练模型的中文自然语言处理平台

论文名称&#xff1a;N-LTP: A Open-source Neural Chinese Language Technology Platform with Pretrained Models论文作者&#xff1a;车万翔&#xff0c;冯云龙&#xff0c;覃立波&#xff0c;刘挺原创作者&#xff1a;冯云龙论文链接&#xff1a;https://arxiv.org/abs/200…

LeetCode 49. 字母异位词分组(哈希)

1. 题目 给定一个字符串数组&#xff0c;将字母异位词组合在一起。字母异位词指字母相同&#xff0c;但排列不同的字符串。 示例:输入: ["eat", "tea", "tan", "ate", "nat", "bat"], 输出: [["ate",…

论文浅尝 - SWJ | 基于知识图谱和注意力图卷积神经网络的可解释零样本学习

论文题目&#xff1a;Explainable Zero-shot Learning via Attentive Graph Convolutional Network and Knowledge Graphs本文作者&#xff1a;耿玉霞&#xff0c;浙江大学在读博士&#xff0c;研究方向为知识图谱、零样本学习、可解释性发表期刊&#xff1a;Semantic Web Jour…

LeetCode 3. 无重复字符的最长子串(滑动窗口+哈希)

1. 题目 给定一个字符串&#xff0c;请你找出其中不含有重复字符的 最长子串 的长度。 示例 1: 输入: "abcabcbb" 输出: 3 解释: 因为无重复字符的最长子串是 "abc"&#xff0c;所以其长度为 3。示例 2: 输入: "bbbbb" 输出: 1 解释: 因为无重…

安装kenlm出现问题的解决方案gcc g++

安装kenlm出现问题的解决方案 apt-get install gcc apt-get install g参考文章&#xff1a;gcc: error trying to exec ‘cc1plus’: execvp: No such file or directory

论文浅尝 - TACL2020 | 改进低资源跨语言实体链接的候选生成问题

论文笔记整理&#xff1a;谭亦鸣&#xff0c;东南大学博士。来源&#xff1a;TACL 2020链接&#xff1a;https://arxiv.org/ftp/arxiv/papers/2003/2003.01343.pdf1.背景介绍跨语言实体链接(Cross-lingual Entity Linking, XEL)旨在利用源语言文本中实体描述(提及)&#xff0c;…

卫星系统——酒店后端全链路日志收集工具介绍

背景 随着酒店业务的高速发展&#xff0c;我们为用户、商家提供的服务越来越精细&#xff0c;系统服务化程度、复杂度也逐渐上升。微服务化虽然能够很好地解决问题&#xff0c;但也有副作用&#xff0c;比如&#xff0c;问题定位。 每次问题定位都需要从源头开始找同事帮我人肉…

拖拽式Vue组件代码生成平台(LCG)新版详细介绍

拖拽式Vue组件代码生成平台是一款小猴自研的Vue代码生成工具&#xff0c;英文全称&#xff1a;Low Code Generator&#xff0c;简称LCG。它也是一种LowCode解决方案。通过它可以快速完成Vue组件的代码骨架搭建&#xff0c;通过减少不必要的重复工作从而带来开发效率的提升。 体…

ImportError: libgthread-2.0.so.0: cannot open shared object file: No such file or directory

apt-get update apt-get install libglib2.0-dev系统&#xff1a;ubuntu16.04

LeetCode 454. 四数相加 II(哈希)

1. 题目 给定四个包含整数的数组列表 A , B , C , D ,计算有多少个元组 (i, j, k, l) &#xff0c;使得 A[i] B[j] C[k] D[l] 0。 为了使问题简单化&#xff0c;所有的 A, B, C, D 具有相同的长度 N&#xff0c;且 0 ≤ N ≤ 500 。所有整数的范围在 -228 到 228 - 1 之间…