首屏性能优化(前方多图预警,手机党慎入^_^)

3,649 阅读18分钟
本文主要通过各种方法对web页面首屏性能进行优化,以期达到最大化减少页面首(白)屏加载时间的目的。当前,页面首屏性能优化主要有4种方式:
  • externals
  • dll
  • gzip
  • CDN
其中,前两者结合了webpack配置尝试对web页面首屏进行优化,后两者结合服务端技术尝试对web页面首屏进行优化。以下将分别介绍这4种方式,并辅以适当的数据分析,以期探索出一种能够最大幅度优化web页面首屏性能的方案。

externals

适用场景

考虑到一些第三方依赖库,在短期内不会发生频繁变动,没必要每次都打包,只需要通过js引入即可。

配置(定位:module.exports->externals)

eject暴露webpack配置之后,module.exports下本来是没有externals配置项的,需要手动新增该配置项。

webpack.config.prod.js

externals: {
	jquery: 'jQuery',
	axios: 'axios',
	moment: 'moment'
}

该配置是将代码中的一部分第三方依赖库,如jquery、axios、moment不进行打包,而是手动在html中引入相应的cdn js,如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="theme-color" content="#000000">
    <!--
      manifest.json provides metadata used when your web app is added to the
      homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
    -->
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json">
    <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
    <!--
      Notice the use of %PUBLIC_URL% in the tags above.
      It will be replaced with the URL of the `public` folder during the build.
      Only files inside the `public` folder can be referenced from the HTML.

      Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
      work correctly both with client-side routing and a non-root public URL.
      Learn how to configure a non-root public URL by running `npm run build`.
    -->
    <title>Gearbest Influencer Program</title>
    <link rel="stylesheet" href="//at.alicdn.com/t/font_975949_zge2u562xp.css">
    <script src="//at.alicdn.com/t/font_975949_zge2u562xp.js"></script>
    <script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
    <script src="https://cdn.bootcss.com/axios/0.18.0/axios.min.js"></script>
    <script src="https://cdn.bootcss.com/moment.js/2.24.0/moment.min.js"></script>
</head>
<body>
<noscript>
    You need to enable JavaScript to run this app.
</noscript>
<div id="main"></div>
<!--
  This HTML file is a template.
  If you open it directly in the browser, you will see an empty page.

  You can add webfonts, meta tags, or analytics to this file.
  The build step will place the bundled scripts into the <body> tag.

  To begin the development, run `npm start` or `yarn start`.
  To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

其中

<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
<script src="https://cdn.bootcss.com/axios/0.18.0/axios.min.js"></script>
<script src="https://cdn.bootcss.com/moment.js/2.24.0/moment.min.js"></script>

这三行代码,分别引入了jquery、axios、moment三个cdn js。像这些第三方依赖库的cdn js的链接地址,可以从www.bootcdn.cn/上获取。但是,也不是所有第三方依赖库可以采用cdn js的方式引入成功的,需要通过实践去验证到底有哪些第三方依赖库是可以通过cdn js的方式引入的。

对比

未采用externals配置的打包图(原始)



采用externals配置的打包图



从图中很明显地发现,引入externals配置打包后,jquery没有被打包进去,且main包大小从902.68KB减小到822.95KB,瘦身幅度达到8.83%

dll

适用场景

考虑到一些第三方依赖库,在短期内不会发生频繁变动,没必要每次都打包,可以通过dll(类似于C/C++语言中的动态链接库的概念)的方式单独引入。该方式为externals的另一种实现方式。

配置(定位:module.exports->plugins)

在目录config下新建webpack.dll.config.js文件,具体内容如下:

// webpack_dll.config.js
const paths = require('./paths');
const DllPlugin = require('webpack/lib/DllPlugin');
module.exports = {
	entry: {
		// 以下基本是全量dll优化的第三方包
		// react: ['react', 'react-dom', 'react-redux', 'redux-saga', 'react-router', 'video-react', 'jquery', 'axios', 'bizcharts'],
		react: ['react', 'react-dom', 'redux-saga', 'video-react'],
		// polyfill: ['core-js/fn/promise', 'whatwg-fetch']
	},
	output: {
		filename: '[name].dll.js',// 该配置上面entry配置项key为react,那么生成filename就是react.dll.js
		path: paths.appPublic,// 生成dll文件的目标路径,本项目保存在public目录下
		library: '_dll_[name]',// dll的全局变量名
	},
	plugins: [
		new DllPlugin({
			name: '_dll_[name]',  // dll的全局变量名
			path: paths.appPublic + '/[name].manifest.json',// 描述生成的manifest文件,同样保存在public目录下
		})
	]
};

以上配置可以将生成的dll文件(react.dll.js)和对应的manifest.json文件(react.manifest.json)保存到public目录下,这样在对整个项目build时可以将该文件直接打包到html文件中。

在package.json中新增dll命令

"scripts": {
  "start": "set PORT=3001 && node scripts/start.js",
  "build": "node scripts/build.js",
  "test": "node scripts/test.js --env=jsdom",
  "lint": "eslint --ext .jsx --ext .js src --fix",
  "dll": "webpack --config config/webpack.dll.config.js"// dll打包命令,--config后的参数为dll配置文件的路径
}

在控制台输入yarn dll,生成dll文件(react.dll.js)


将dll文件(react.dll.js)手动引入到对应html文件body标签末尾

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="theme-color" content="#000000">
    <!--
      manifest.json provides metadata used when your web app is added to the
      homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
    -->
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json">
    <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
    <!--
      Notice the use of %PUBLIC_URL% in the tags above.
      It will be replaced with the URL of the `public` folder during the build.
      Only files inside the `public` folder can be referenced from the HTML.

      Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
      work correctly both with client-side routing and a non-root public URL.
      Learn how to configure a non-root public URL by running `npm run build`.
    -->
    <title>Gearbest Influencer Program</title>
    <link rel="stylesheet" href="//at.alicdn.com/t/font_975949_zge2u562xp.css">
    <script src="//at.alicdn.com/t/font_975949_zge2u562xp.js"></script>
  </head>
  <body>
    <noscript>
      You need to enable JavaScript to run this app.
    </noscript>
    <div id="root"></div>
    <!--
      This HTML file is a template.
      If you open it directly in the browser, you will see an empty page.

      You can add webfonts, meta tags, or analytics to this file.
      The build step will place the bundled scripts into the <body> tag.

      To begin the development, run `npm start` or `yarn start`.
      To create a production bundle, use `npm run build` or `yarn build`.
    -->
    <script src="%PUBLIC_URL%/react.dll.js"></script>
  </body>
</html>

其中,PUBLIC_URL即public目录。

yarn build之后,打包后的文件目录结构如下:

build
├─ asset-manifest.json
├─ favicon.ico
├─ index.html
├─ login.html
├─ main.html
├─ manifest.json
├─ react.dll.js
├─ react.manifest.json
├─ register.html
├─ service-worker.js
└─ static
       ├─ css
       │    ├─ index.7dc833cb.css
       │    ├─ index.7dc833cb.css.map
       │    ├─ login.2ff64c8e.css
       │    ├─ login.2ff64c8e.css.map
       │    ├─ main.c8e56e47.css
       │    ├─ main.c8e56e47.css.map
       │    ├─ register.e54b83db.css
       │    └─ register.e54b83db.css.map
       ├─ js
       │    ├─ index.e6968086.js
       │    ├─ index.e6968086.js.map
       │    ├─ login.67bdfb3a.js
       │    ├─ login.67bdfb3a.js.map
       │    ├─ main.4a5c410c.js
       │    ├─ main.4a5c410c.js.map
       │    ├─ register.5d9dd95a.js
       │    └─ register.5d9dd95a.js.map
       └─ media
              ├─ GB_bg.c4c2913b.png
              ├─ iconfont.26b4e298.eot
              ├─ iconfont.3e355800.svg
              ├─ iconfont.5cd1f541.ttf
              ├─ img07.c2e13bab.png
              ├─ img08.22d05748.png
              └─ login_bg.165cc596.jpg

可以发现,打包后的build文件夹中已经有了react.dll.js和react.manifest.json文件,这两个文件是没有经过打包,直接从public文件夹中copy过来的。

对比

采用dll配置的打包图



从打包图中,可以明显地看出来原先两个第三方依赖库react-dom和video-react,并没有打包进来,且index包大小从460.75KB减小到408.67KB,瘦身幅度达到11.3%

对于externals和dll优化的一些思考

采用externals和dll进行webpack优化的初衷都是提取一些公共的第三方依赖包,单独由cdn引入或以动态链接库的方式引入项目中来,使其不用每次都被打包进主包(build/dist文件夹)。但是,仍然需要手动在html文件中引入,在页面加载渲染的时候不仅要下载主css、主js,还要下载对应的cdn js或对应的dll js,这无疑可能会使得页面的白屏时间更长。

gzip

可以发现,上述两种方法均是从客户端(前端第三方依赖库的分离)的角度出发对页面首屏性能优化的尝试,不同的是,gzip(以及接下来要介绍的cdn方法)是从服务端的角度出发对页面首屏进行优化的一种方案。gzip的重点在于对nginx进行配置优化,以下将以本人的nginx为例对该方案进行详细介绍。

nginx主配置文件


nginx.conf


我的前端项目nginx配置文件node_react.conf在/etc/nginx/vhost目录下,可以看到,我们通过include的方式引入我的前端项目nginx配置文件,这种写法更加便于管理和维护,能够降低配置污染。进入该目录。


该目录下,主要关注node_react.conf和gzip.conf,其中前者为我的前端项目nginx配置文件,后者为gzip配置文件。

我的前端项目nginx配置文件

node_react.conf


进一步查看gzip配置文件

gzip配置文件

gzip.conf


#开启gzip	
gzip on;
gzip_min_length 1k;
gzip_buffers 4 16k;
#gzip_http_version 1.0;
gzip_comp_level 2;
gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php;
gzip_vary off;
gzip_disable "MSIE [1-6]\.";

#针对不同格式文件过滤进行gzip缓存
location ~* ^.+\.(ico|gif|jpg|jpeg|png|webp)$ { 
	access_log   off; 
	expires      30d;
}	

location ~* ^.+\.(css|js|txt|xml|swf|wav)$ {
	access_log   off;
	expires      24h;
}	

location ~* ^.+\.(html|htm)$ {
	expires      1h;
}
#开启gzip

确定是否开启gzip的方法

打开customize control DevTools,切换到Network,找到Content-Encoding列。


如果找不到Content-Encoding列,可按照下图中的方法操作


下图为“未开启gzip”的DevTools截图


下图为“开启gzip”的DevTools截图


从图中可以看出,在DevTools->Network中Content-Encoding列显示gzip字样,即可确定服务端开启了gzip。

确定是否开启缓存的方法


怎么确定页面首屏时间和页面白屏时间

也许此时读者会有疑问啦!我们可以通过什么手段准确地得到页面首屏时间和页面白屏时间的数据呢?怎么判断什么时候是页面首屏加载结束的时间点呢?其实刚开始我也有这些疑问,后来经过查阅资料和对谷歌浏览器customize control DevTools的一番研究之后,得出了一种简单可行的方法推荐给大家。

  • 打开谷歌浏览器,Ctrl+Shift+Delete清缓存(注意记录页面首屏时间和页面白屏时间前一定要清缓存),点击F12,进入customize control DevTools界面,切换Performance面盘,点击已提前保存的书签打开网页或输入网址,同时点击DevTools界面左上方的灰色圆形按钮,随即灰色原型按钮变为红色闪亮,开始记录页面加载时间,此时网页页面title处会出现loading一直转圈。


  • 待网页页面title处loading转圈结束,出现页面favicon图标,随即点击DevTools界面左上方红色闪亮圆形按钮,停止记录页面加载时间,同时下方出现时间线图。将左侧时间游标移至最右边,右侧时间游标移至最右边,观察下方Range处时间段,得到页面首屏时间。


  • 再将右侧时间游标移至中间绿色竖线处,观察下方Range处时间段,得到页面白屏时间。


到此,一波页面首(白)屏时间之旅正式宣告结束,不知道您学会没呢?当然,我们也可以结合特定工具对我们的网页进行性能分析,以下推荐给大家一些谷歌插件:

  • PageSpeed Insights (with PNaCl)
  • app.telemetry Page Speed Monitor
  • SEO Analysis & Website Review by WooRank


为了节省篇幅,在此就不对各个插件进行详细介绍了,大家可以自行研究。

方案对比

经过测试,我们发现开启gzip后,在客户端请求服务器前端静态资源(css、js、图片)时可以对其进行高质量压缩,大大降低带宽压力,从而可以大幅度减小页面白屏时间。以下将从一组数据对该结论进行验证。在给出测试数据和图表之前,需要对下文提及的方案1、方案2、方案3、方案4、方案5、方案6、方案7-1、方案7-2、方案8进行简要描述。

  • 方案1:原始webpack(eject后,未做任何处理)+未开启gzip
  • 方案2:原始webpack(eject后,未做任何处理)+开启gzip
  • 方案3:原始webpack(eject后,未做任何处理)+未开启gzip+对GB_bg.png/img07.png/img08.png进行cdn地址替换+【对打包后的index.js进行cdn地址替换】
  • 方案4:原始webpack(eject后,未做任何处理)+开启gzip+对GB_bg.png/img07.png/img08.png进行cdn地址替换+【对打包后的index.js进行cdn地址替换】
  • 方案5:原始webpack(eject后,未做任何处理)+开启gzip+对GB_bg.png/img07.png/img08.png进行cdn地址替换+【不对打包后的index.js进行cdn地址替换】
  • 方案6:原始webpack(eject后,未做任何处理)+开启gzip+img07/img08图片懒加载
  • 方案7-1:原始webpack(eject后,未做任何处理)+开启gzip+img07/img08图片懒加载+GB_bg.png->GB_bg.jpg
  • 方案7-2:原始webpack(eject后,未做任何处理)+开启gzip+img07/img08图片懒加载+GB_bg.png->GB_bg.webp

  • 方案8:原始webpack(eject后,未做任何处理)+开启gzip+【不对打包后的index.js进行cdn地址替换】+img07/img08图片懒加载+GB_bg.png->GB_bg.webp后替换cdn地址

未开启gzip




开启gzip




以上这组数据对我的前端项目中的两张图片和主css文件进行了gzip性能对比测试,设备为本人笔记本电脑,网络为住所WIFI。

从数据中可以看出,开启gzip较之未开启gzip,优化之处在于大幅度降低了下载静态资源的时间。其中,index.css的下载时间下降了87.88%,index.js的下载时间下降了59.41%,而且index.css的文件大小由175KB减小到33KB,瘦身了81.14%,index.js的文件大小由1536KB减小到525KB,瘦身了65.82%

进一步地,我们只关注本组数据的页面首屏时间和页面白屏时间,可以看到前者的指标数据从25.208秒下降到了18.253秒,下降幅度达到了27.59%,后者的指标数据从14.454秒下降到了6.02秒,下降幅度达到了58.35%,特别是页面白屏时间的下降幅度已经是一个很不错的成绩了。

分析

我们已经知道,服务端gzip方案确实能够大幅降低页面白屏时间,大大优化首屏性能,那么为什么可以达到这样的性能优化效果呢?在回答这个问题之前,我们首先思考一下造成前端页面首屏加载慢的原因到底是什么呢?仔细琢磨一下,浏览器加载首屏之前,首先会去服务器下载主css、主js文件,这两者之中任何一个文件下载速度慢了都会导致页面阻塞,造成页面首屏加载慢。而服务端gzip刚好可以解决这个问题,它采用高效的压缩算法(应该也有一部分缓存作用,这部分暂不探究),能够大幅度降低服务端静态资源到达客户端的速度(或体量),起到加速(或瘦身)器的作用。

CDN

什么是CDN

CDN的全称是Content Delivery Network,即内容分发网络。CDN是构建在现有网络基础之上的智能虚拟网络,依靠部署在各地的边缘服务器,通过中心平台的负载均衡、内容分发、调度等功能模块,使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。CDN的关键技术主要有内容存储和分发技术。---引自百度

CDN在前端领域的应用场景

CDN应用于前端领域,主要是可以起到加速下载静态资源的过程,从而达到首屏优化的效果。一般地,在CDN的加持下,静态资源的下载过程将被大幅度加速,从而达到优化首屏性能的目的。结合实际情况,我们可以选择对整个前端项目进行全局CDN加速,也可以对部分js文件或图片进行局部CDN加速。

方案对比与数据分析

以下将给出一组测试数据,测试设备为本人笔记本电脑,测试网络为本人住所WIFI。

未开启gzip+cdn(保留异常数据)




未开启gzip+cdn(去除异常数据)




从以上测试数据可以看出,cdn方案虽然能够达到较理想的性能优化效果(去除异常数据后,相较于方案1,页面首屏时间降低了50.02%,页面白屏时间降低了61.94%),但是稳定性不足,12次测试就有两次数据异常,异常率高达16.67%,甚至有一次3分钟还未加载出首屏。

为了达到最大化的性能优化效果,我们可以将gzip与cdn结合运用到我们的前端项目中,以下给出一组这种结合的测试数据。

开启gzip+cdn(保留异常数据)




开启gzip+cdn(去除异常数据)




该方案虽然可以达到较大的优化结果,但是稳定性仍然有待提高(异常率高达23.08%),这取决于cdn网络的稳定性,如果cdn网络的稳定性达到理想程度,首屏优化效果至少还可以再提升50%以上。此处为了折中稳定性和优化性能,给出了方案5,即“原始webpack(eject后,未做任何处理)+开启gzip+对GB_bg.png/img07.png/img08.png进行cdn地址替换+【不对打包后的index.js进行cdn地址替换】”的局部cdn方案,以下为该方案的测试数据。

开启gzip+局部cdn




第一轮性能对比

综合上述五种方案,给出第一轮性能对比。



从柱状图中可以看出,方案4虽然性能提升最大,但是较不稳定(取决于cdn网络的稳定性),方案5的性能提升幅度仅次于方案4,且较稳定,很好地折中了稳定性和性能提升两方面,对于cdn网络待提升的现实应用场景下,不失为一个理想的解决方案。

图片懒加载

如果首屏有些图片不需要在可视区立即展示,我们可以采用图片懒加载的方式减少首屏加载的时间。在本项目中,有两张图片img07和img08不需要在首屏可视区中立即展示,可以对其进行懒加载,即鼠标滑动进入图片所在区域才加载。参考延迟加载图像和视频,对本项目两张图片img07、img08进行懒加载,并对首屏性能进行了一组测试,数据如下:




从数据中可以看出,该方案相较于方案2,页面首屏时间减少1.033秒,下降幅度达5.66%,这段减少的时间即为图片懒加载省下来的时间。

转换图片格式

通过将图片转化为编码性能更高、体积更小的格式,也能够有效减少首屏加载时间,如PNG格式转化为jpeg格式或webP格式后,文件将变得更小。我们通过一些手段将PNG格式图片转化为jpeg格式或webP格式,这里我推荐一款谷歌扩展工具Convertio,它可以帮助我们完成多种图片格式的转换,非常方便。利用该工具,我特地将本项目中首屏页面中的一张最大的背景图GB_bg.png转换为了jpeg格式和webP格式,如下图


从图中可以看出,相比于png格式的原图,jpeg格式转换图的压缩率达到了39.22%,webP格式转换图的压缩率更是达到了惊人的88.12%

进一步地,我们在对img07和img08采取懒加载处理的基础上,将jpeg转换图和webP转换图分别应用于项目中,得到了一组测试数据。

jpeg转换图




webP转换图




可以看出,将转换后的jpeg格式图片和webP格式图片应用到项目中,与方案2相比,相同内容的图片下载时间分别减少了49.12%92.65%,最终表现在页面首屏时间上的提升则是14.1%42.5%,这也有力地证实了将图片转换成编码性能更高、体积更小的格式,能够有效减少首屏加载时间的结论。

webP格式图片cdn

我们不妨继续折腾一下,将上述转换的webP图片用cdn地址替换,对页面首屏性能进行了一组测试。




很不幸,我们看到,将转换的webP格式图片用cdn地址替换后,页面首屏时间并没有下降,而是有了小幅上升,可见不是所有的资源都适合cdn。仔细分析一波,转换的webP格式图片只有84.8KB,直接从本地服务器下载的速度可能并不比从cdn服务器下载的速度慢,这才导致了页面首屏时间不降反升的现象。

终极性能对比

好了,说了这么多,给出了这么多方案,为了能够更加宏观地分析方案的整体性能,我们对以上八种方案进行了终极性能对比。


从终极性能对比图中,我们可以看到方案4的页面首屏性能提升最为明显,平均只需要9.656秒即可完成首屏加载,但是该方案的稳定性不足,但是如果cdn网络足够稳定的话,页面首屏性能仍有较大的提升空间。方案7-2的页面首屏性能提升仅次于方案4,且具有较强的稳定性,该方案在cdn网络性能受限的应用场景下具备较强的竞争力。当然,具体选择什么方案,需要针对应用场景和现状具体问题具体分析,仁者见仁智者见智,永远没有最好的方案,只有更好的方案。

参考文章

「性能优化」首屏时间从12.67s到1.06s,我是如何做到的?

淘宝首页性能优化实践

三十分钟掌握Webpack性能优化

webpack优化

加速nginx: 开启gzip和缓存