在开发 web 应用程序时候,性能都是必不可少的话题。而大部分的前端优化机制都已经被集成到前端打包工具 webpack 中去了,固然,事实上仍旧会有一些有趣的机制能够帮助 web 应用进行性能提高,在这里咱们来聊一聊可以优化 web 应用程序的一些机制,同时也谈一谈这些机制背后的原理。css
在讲解这些机制前,先来谈一个 Chrome 工具 Corverage。该工具能够帮助查找在当前页面使用或者未使用的 JavaScript 和 CSS 代码。html
工具的打开流程为:前端
webpackjsvue
这里以淘宝网为例子,介绍一下如何使用node
上面两张分别为 reload 与 record 点击后的分析。webpack
其中从左到右分别为git
左下角有一份总述。说明在当前页面加载的资源大小以及没有使用的百分比。能够看到淘宝网对于首页代码的未使用率仅仅只有 36%。github
介绍该功能的目的并非要求各位重构代码库以便于每一个页面仅仅只包含所需的 js 与 css。这个是难以作到的甚至是不可能的。可是这种指标能够提高咱们对当前项目的认知以便于性能提高。web
提高代码覆盖率的收益是全部性能优化机制中最高的,这意味着能够加载更少的代码,执行更少的代码,消耗更少的资源,缓存更少的资源。vue-router
通常来讲,咱们基本上都会使用 Vue,React 以及相对应的组件库来搭建 SPA 单页面项目。可是在构建时候,把这些框架代码直接打包到项目中,并不是是一个十分明智的选择。
咱们能够直接在项目的 index.html 中添加以下代码
<script src="//cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.runtime.min.js" crossorigin="anonymous"></script> <script src="//https://cdn.jsdelivr.net/npm/vue-router@3.1.3/dist/vue-router.min.js" crossorigin="anonymous"></script>
而后能够在 webpack.config.js 中这样配置
module.exports = { //... externals: { 'vue': 'Vue', 'vue-router': 'VueRouter', } };
webpack externals 的做用是 不会在构建时将 Vue 打包到最终项目中去,而是在运行时获取这些外部依赖项。这对于项目初期没有实力搭建自身而又须要使用 CDN 服务的团队有着不错的效果。
这些项目被打包成为第三方库的时候,同时还会以全局变量的形式导出。从而能够直接在浏览器的 window 对象上获得与使用。便是
window.Vue // ƒ bn(t){this._init(t)}
这也就是为何咱们直接能够在 html 页面中直接使用
<div id="app"> {{ message }} </div> // Vue 就是 挂载到 window 上的,因此能够直接在页面使用 var app = new Vue({ el: '#app', data: { message: 'Hello Vue!' } })
此时咱们能够经过 webpack Authoring Libraries 来了解如何利用 webpack 开发第三方包。
对于这种既没法进行代码分割又没法进行 Tree Shaking 的依赖库而言,把这些需求的依赖库放置到公用 cdn 中,收益是很是大的。
对于相似 Vue React 此类库而言,CDN 服务出现问题意味着彻底没法使用项目。须要常常浏览所使用 CDN 服务商的公告(再也不提供服务等公告),以及在代码中添加相似的出错弥补方案。
<script src="//cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.runtime.min.js" crossorigin="anonymous"></script> <script>window.Vue || ...其余处理 </script>
咱们能够利用 webpack 动态导入,能够在须要利用代码时候调用 getComponent。在此以前,须要对 webpack 进行配置。具体参考 webpack dynamic-imports。
在配置完成以后,咱们就能够写以下代码。
async function getComponent() { const element = document.createElement('div'); /** webpackChunkName,相同的名称会打包到一个 chunk 中 */ const { default: _ } = await import(/* webpackChunkName: "lodash" */ 'lodash'); element.innerHTML = _.join(['Hello', 'webpack'], ' '); return element; } getComponent().then(component => { document.body.appendChild(component); });
经过动态导入配置,能够搞定多个 chunk,在须要时候才会加载然后执行。对于该用户不会使用的资源(路由控制,权限控制)不会进行加载,从而直接提高了代码的覆盖率。
Tree Shaking,能够理解为死代码消除,即不须要的代码不进行构建与打包。但当咱们使用动态导入时候,没法使用 Tree Shaking 优化,由于二者直接按存在着兼容性问题。由于 webpack 没法假设用户如何使用动态导入的状况。
基础代码X 模块A 模块B ----------------------------------- 业务代码A 业务代码B 业务代码...
当在业务中使用多个异步块时后,业务代码A 需求 模块A,业务代码 B 需求 模块B,可是 webpack 没法去假设用户在代码中 A 与 B 这两个模块在同一时间是互斥仍是互补。因此必然会假设同时能够加载模块 A 与 B,此时基础代码 X 出现两个导出状态,这个是作不到的!从这方面来讲,动态导入和 Tree Shaking 很难兼容。具体能够参考 Document why tree shaking is not performed on async chunks 。
固然,利用动态导入,也会有必定的性能下降,毕竟一个是本地函数调用,另外一个涉及网络请求与编译。可是与其说这是一种缺陷,倒不如说是一种决策。到底是哪种对自身的项目帮助更大?
在普通的业务代码咱们可使用动态导入,在当今的前端项目中,总有一些库是咱们必需而又使用率很低的库,好比在只会在统计模块出现的 ECharts 数据图表库,或者只会在文档或者网页编辑时候出现的富文本编辑器库。
对于这些苦库其实咱们可使用页面或组件挂载时候 loadjs 加载。由于使用动态导入这些第三方库没有 Tree shaking 加强,因此其实效果差很少,可是 loadjs 能够去取公用 CDN 资源。具体能够参考 github loadjs 来进行使用。由于该库较为简单,这里暂时就不进行深刻探讨。
由于不管是使用 webpack externals 或者 loadjs 来使用公用 cdn 都是一种折衷方案。若是公司能够花钱购买 oss + cdn 服务的话,就能够直接将打包的资源托管上去。
module.exports = { //... output: { // 每一个块的前缀 publicPath: 'https://xx/', chunkFilename: '[id].chunk.js' } }; // 此时打包出来的数据前缀会变为 <script src=https://xx/js/app.a74ade86.js></script>
此时业务服务器仅仅只须要加载 index.html。
若是不须要在浏览器的首屏中使用脚本。能够利用浏览器新增的 prefetch 延时获取脚本。
下面这段代码告诉浏览器,echarts 将会在将来某个导航或者功能中要使用到,可是资源的下载顺序权重比较低。也就是说prefetch一般用于加速下一次导航。被标记为 prefetch 的资源,将会被浏览器在空闲时间加载。
<link rel="prefetch" href="https://cdn.jsdelivr.net/npm/echarts@4.3.0/dist/echarts.min.js"></link>
该功能也适用于 html 以及 css 资源的预请求。
instant.page 是一个较新的功能库,该库小而美。而且无侵入式。
只要在项目的 </body> 以前加入如下代码,便会获得收益。
<script src="//instant.page/2.0.1" type="module" defer integrity="sha384-4Duao6N1ACKAViTLji8I/8e8H5Po/i/04h4rS5f9fQD6bXBBZhqv5am3/Bf/xalr"></script>
该方案不适合单页面应用,可是该库很棒的运用了 prefetch,是在你悬停于连接超过65ms 时候,把已经放入的 head 最后的 link 改成悬停连接的 href。
下面代码是主要代码
// 加载 prefetcher const prefetcher = document.createElement('link') // 查看是否支持 prefetcher const isSupported = prefetcher.relList && prefetcher.relList.supports && prefetcher.relList.supports('prefetch') // 悬停时间 65 ms let delayOnHover = 65 // 读取设定在 脚本上的 instantIntensity, 若是有 修改悬停时间 const milliseconds = parseInt(document.body.dataset.instantIntensity) if (!isNaN(milliseconds)) { delayOnHover = milliseconds } // 支持 prefetch 且 没有开启数据保护模式 if (isSupported && !isDataSaverEnabled) { prefetcher.rel = 'prefetch' document.head.appendChild(prefetcher) ... // 鼠标悬停超过 instantIntensit ms || 65ms 改变 href 以便预先获取 html mouseoverTimer = setTimeout(() => { preload(linkElement.href) mouseoverTimer = undefined }, delayOnHover) ... function preload(url) { prefetcher.href = url }
延时 prefetch ? 仍是在鼠标停留的时候去加载。不得不说,该库利用了不少浏览器新的的机制。包括使用 type=module 来拒绝旧的浏览器执行,利用 dataset 读取 instantIntensity 来控制延迟时间。
认识到这个库是在 v8 关于新版本的文章中,在 github 中被标记为 UNMAINTAINED 再也不维护,可是了解与学习该库仍旧有其的价值与意义。该库的用法十分简单粗暴。竟然只是把函数改成 IIFE(当即执行函数表达式)。
用法以下:
optimize-js input.js > output.js
Example input:
!function (){}() function runIt(fun){ fun() } runIt(function (){})
Example output:
!(function (){})() function runIt(fun){ fun() } runIt((function (){}))
在 v8 引擎内部(不只仅是 V8,在这里以 v8 为例子),位于各个编译器的前置Parse 被分为 Pre-Parse 与 Full-Parse,Pre-Parse 会对整个 Js 代码进行检查,经过检查能够直接断定存在语法错误,直接中断后续的解析,在此阶段,Parse 不会生成源代码的AST结构。
// This is the top-level scope. function outer() { // preparsed 这里会预分析 function inner() { // preparsed 这里会预分析 可是不会 全分析和编译 } } outer(); // Fully parses and compiles `outer`, but not `inner`.
可是若是使用 IIFE,v8 引擎直接不会进行 Pre-Parsing 操做,而是当即彻底解析并编译函数。能够参考Blazingly fast parsing, part 2: lazy parsing
快!即便在较新的 v8 引擎上,咱们能够看到 optimize-js 的速度依然是最快的。更不用说在国内浏览器的版本远远小于 v8 当前版本。与后端 node 不一样,前端的页面生命周期很短,越快执行越好。
可是一样的,任何技术都不是银弹,直接彻底解析和编译也会形成内存压力,而且该库也不是 js 引擎推荐的用法。相信在不远的将来,该库的收益也会逐渐变小,可是对于某些特殊需求,该库的确会又必定的助力。
此时咱们在谈一次代码覆盖率。若是咱们能够在首屏记载的时候能够达到很高的代码覆盖率。直接执行即是更好的方式。在项目中代码覆盖率越高,越过 Pre-Parsing 让代码尽快执行的收益也就越大。
若是写过前端,就不可能不知道 polyfill。各个浏览器版本不一样,所须要的 polyfill 也不一样,
Polyfill.io是一项服务,可经过选择性地填充浏览器所需的内容来减小 Web 开发的烦恼。Polyfill.io读取每一个请求的User-Agent 标头,并返回适合于请求浏览器的polyfill。
若是是最新的浏览器且具备 Array.prototype.filter
https://polyfill.io/v3/polyfill.min.js?features=Array.prototype.filter /* Disable minification (remove `.min` from URL path) for more info */
若是没有 就会在 正文下面添加有关的 polyfill。
国内的阿里巴巴也搭建了一个服务,能够考虑使用,网址为 https://polyfill.alicdn.com/p...
使用新的 DOM API,能够有条件地加载polyfill,由于能够在运行时检测。可是,使用新的 JavaScript 语法,这会很是棘手,由于任何未知的语法都会致使解析错误,而后全部代码都不会运行。
该问题的解决方法是
<script type="module">。
早在 2017 年,我便知道 type=module 能够直接在浏览器原生支持模块的功能。具体能够参考 JavaScript modules 模块。可是当时感受只是这个功能很强大,并无对这个功能产生什么解读。可是却没有想到能够利用该功能识别你的浏览器是否支持 ES2015。
每一个支持 type="module" 的浏览器都支持你所熟知的大部分 ES2015+ 语法!!!!!
例如
所以,利用该特性,彻底能够去作优雅降级。在支持 type=module 提供所属的 js,而在 不支持的状况下 提供另外一个js。具体能够参考 Phillip Walton 精彩的博文,这里也有翻译版本 https://jdc.jd.com/archives/4911.
若是当前项目已经开始从 webpack 阵营转到 Vue CLI 阵营的话,那么恭喜你,上述解决方案已经被内置到 Vue CLI 当中去了。只须要使用以下指令,项目便会产生两个版本的包。
vue-cli-service build --modern
具体能够参考 Vue CLI 现代模式
提高代码覆盖率,直接使用原生的 await 等语法,直接减小大量代码。
提高代码性能。以前 v8 用的时 Crankshaft 编译器,随着时间的推移,该编译器由于没法优化现代语言特性而被抛弃,以后 v8 引入了新的 Turbofan 编译器来对新语言特性进行支持与优化,以前在社区中谈论的 try catch, await,JSON 正则等性能都有了很大的提高。具体能够时常浏览 v8 blog 来查看功能优化。
Writing ES2015 code is a win for developers, and deploying ES2015 code is a win for users.
无,实在考虑不出有什么很差。
若是你以为这篇文章不错,但愿能够给与我一些鼓励,在个人 github 博客下帮忙 star 一下。
博客地址
webpackjs 中文文档
Blazingly fast parsing, part 2: lazy parsing
Polyfill.io
JavaScript modules 模块
deploying-es2015-code-in-production-today
Vue CLI 现代模式
v8 blog