首页白屏优化实践

自从前端三大框架React、Vue、Angular面世以来,前端开发逐渐趋向规范化、统一化,大多数时候新建前端项目,首先想到使用的技术一定是三大框架之一,框架给前端开发带来了极大的便利和规范,但是由于这三大框架都是JS驱动,在JS没有解析加载完成之前页面无法展示,会处于长时间的白屏,带来了一定的用户体验问题,接下来本篇文章会介绍本人最近在白屏优化时遇到的一些问题和思考

SSR

想到白屏问题,首先想到的解决方案一般都是服务端渲染,在服务端将渲染逻辑处理好,然后将处理好的HTML直接返回给前端展示,这样就可以解决白屏的问题,也可以解决seo的问题,因为不需要动态获取数据了,但是,这和我早期的写后端时的开发模式很像,前端和后端关联在了一起,不利于维护,同时,对于前端工程师来说,要求变高来,需要了解一定的后端知识,虽然有类似Nuxt.js这类的SSR框架帮我们简化了服务端的部分,但是在要做定制或是解决bug时还是无法避免要对服务端部分进行调试、维护,成本颇高,还有需要考虑的服务端渲染会增加服务器压力,要处理并发、运行速度问题等等

预渲染

这个方案是相对简单直接的一个解决办法,尝试成本也比较低,这里介绍如何用prerender-spa-plugin做预渲染,这样就可以在浏览器进行渲染,而不需要将Vue或者React代码部署到服务器上,以vue-cli3的官方demo为例做配置,看具体的配置文件:

const path = require('path')
const PrerenderSPAPlugin = require('prerender-spa-plugin')
const Renderer = PrerenderSPAPlugin.PuppeteerRenderer
module.exports = {
  configureWebpack: config => {
    let plugins = []
    plugins.push(new PrerenderSPAPlugin({
      staticDir: path.resolve(__dirname, 'dist'),
      routes: ['/', '/about'],
      minify: {
        collapseBooleanAttributes: true,
        collapseWhitespace: true,
        decodeEntities: true,
        keepClosingSlash: true,
        sortAttributes: true
      },
      renderer: new Renderer({
        renderAfterDocumentEvent: 'custom-render-trigger'
      })
    }))
    config.plugins = [
      ...config.plugins,
      ...plugins
    ]
  }
}

上面代码是常用prerender-spa-plugin的配置,staticDir预渲染输出的文件地址,routes要做预渲染的路由,minify压缩相关的配置,renderer渲染引擎相关的配置,可以传入自定以的渲染引擎或者直接使用默认的PuppeteerRenderer,renderAfterDocumentEvent是渲染引擎配置中的一个属性,指当某个事件触发时才执行预渲染,这里 有关于渲染引擎的完整属性介绍,这很重要,尤其是对一些特定场景的下的需求,当然简单场景下完全可以不配置renderer渲染引擎选项,直接用默认选项。
接下来执行编译,看看会发生什么?

dist目录下会生成路由对应的文件夹,打开index.html

  <div id="app">
   <div id="nav">
    <a href="/" class="router-link-exact-active router-link-active">Home</a> | 
    <a href="/about" class="">About</a>
   </div>
   <div class="home">
    <img alt="Vue logo" src="/img/logo.82b9c7a5.png" />
    <div class="hello" data-v-7b2de9b7="">
     <h1 data-v-7b2de9b7="">Welcome to Your Vue.js App</h1>
     <p data-v-7b2de9b7="">For a guide and recipes on how to configure / customize this project,<br data-v-7b2de9b7="" />check out the <a href="https://cli.vuejs.org" data-v-7b2de9b7="" rel="noopener" target="_blank">vue-cli documentation</a>.</p>
     <h3 data-v-7b2de9b7="">Installed CLI Plugins</h3>
     <ul data-v-7b2de9b7="">
      <li data-v-7b2de9b7=""><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" data-v-7b2de9b7="" rel="noopener" target="_blank">babel</a></li>
      <li data-v-7b2de9b7=""><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" data-v-7b2de9b7="" rel="noopener" target="_blank">eslint</a></li>
      <li data-v-7b2de9b7=""><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-unit-jest" data-v-7b2de9b7="" rel="noopener" target="_blank">unit-jest</a></li>
     </ul>
     <h3 data-v-7b2de9b7="">Essential Links</h3>
     <ul data-v-7b2de9b7="">
      <li data-v-7b2de9b7=""><a href="https://vuejs.org" data-v-7b2de9b7="" rel="noopener" target="_blank">Core Docs</a></li>
      <li data-v-7b2de9b7=""><a href="https://forum.vuejs.org" data-v-7b2de9b7="" rel="noopener" target="_blank">Forum</a></li>
      <li data-v-7b2de9b7=""><a href="https://chat.vuejs.org" data-v-7b2de9b7="" rel="noopener" target="_blank">Community Chat</a></li>
      <li data-v-7b2de9b7=""><a href="https://twitter.com/vuejs" data-v-7b2de9b7="" rel="noopener" target="_blank">Twitter</a></li>
      <li data-v-7b2de9b7=""><a href="https://news.vuejs.org" data-v-7b2de9b7="" rel="noopener" target="_blank">News</a></li>
     </ul>
     <h3 data-v-7b2de9b7="">Ecosystem</h3>
     <ul data-v-7b2de9b7="">
      <li data-v-7b2de9b7=""><a href="https://router.vuejs.org" data-v-7b2de9b7="" rel="noopener" target="_blank">vue-router</a></li>
      <li data-v-7b2de9b7=""><a href="https://vuex.vuejs.org" data-v-7b2de9b7="" rel="noopener" target="_blank">vuex</a></li>
      <li data-v-7b2de9b7=""><a href="https://github.com/vuejs/vue-devtools#vue-devtools" data-v-7b2de9b7="" rel="noopener" target="_blank">vue-devtools</a></li>
      <li data-v-7b2de9b7=""><a href="https://vue-loader.vuejs.org" data-v-7b2de9b7="" rel="noopener" target="_blank">vue-loader</a></li>
      <li data-v-7b2de9b7=""><a href="https://github.com/vuejs/awesome-vue" data-v-7b2de9b7="" rel="noopener" target="_blank">awesome-vue</a></li>
     </ul>
    </div>
   </div>
  </div>

为了方便,这里只贴了app节点里的代码,以往在没有使用预渲染插件时app节点里面是空的没有内容,从加载index.html文件开始到js文件解析完成之前,由于app节点里面是空的,因此页面会处于白屏状态,但是预渲染插件在编译阶段就将对应的路由编译好插入到app节点,这样就能在js文件解析过程中有内容展示,js解析完成后,Vue会将app节点内的内容替换成Vue渲染好的内容,来看看chrome调试下渲染有什么区别:
常规渲染:

预渲染:

利用chrome浏览器的加载截屏功能可以看出常规渲染时会有明显的白屏时间,而预渲染则不会产生白屏,那么预渲染有什么缺点呢?

  • 动态数据无法展示,不同的用户看到的都是同样的页面
  • 路由很多时,代码构建时间太长
  • 用户容易误操作,由于预渲染时js还没有加载,因此展示出来的内容没有js的交互逻辑,比如按钮点击,在js加载完成之前用户点击是没有反应的
  • 预加载内容很少,当页面有内容是依赖动态数据加载时,在编译时是无法加载出动态数据的,因此会导致这部分内容编译不出来

骨架屏

骨架屏的实现原理和预加载类似,都是利用了Puppeteer爬取页面的功能,Puppeteer是chrome出的一个headlessChromenode库,提供了API可以抓取SPA并生成预渲染内容,和预加载不太一样的是骨架屏技术会在Puppeteer生成内容后,利用算法将生成的内容进行替换,生成骨架页面,page-skeleton-webpack-plugin是一个用来生成骨架屏的webpack插件,接下来就来看看怎么使用,还是以vue-cli3生成的官方项目为例:

<div id="app"><!-- shell --></div>
const SkeletonPlugin = require('page-skeleton-webpack-plugin').SkeletonPlugin
const path = require('path')
module.exports = {
  publicPath: '/',
  outputDir: 'dist',
  configureWebpack: config => {
    let plugins = []
    plugins.push(new SkeletonPlugin({
      pathname: path.resolve(__dirname, './shell'), // pathname为来存储 shell 文件的地址
      staticDir: path.resolve(__dirname, './dist'), // 最好和 `output.path` 相同
      routes: ['/', '/about'], // 将需要生成骨架屏的路由添加到数组中
      port: '7890'
    }))
    config.plugins = [
      ...config.plugins,
      ...plugins
    ]
  },
  chainWebpack: config => {
    if (process.env.NODE_ENV === 'production') {
      config.plugin('html').tap(opts => {
        console.log(opts[0])
        opts[0].minify.removeComments = false
        return opts
      })
    }
  }
}

上面例子是对page-skeleton-webpack-plugin的简单配置,想要完整的配置可以自行前往github获取,需要注意的是这段代码:

chainWebpack: config => {
    if (process.env.NODE_ENV === 'production') {
      config.plugin('html').tap(opts => {
        console.log(opts[0])
        opts[0].minify.removeComments = false
        return opts
      })
    }
  }

这是修改了vue-cli3中集成的html-webpack-plugin的压缩配置,将移除注释去掉了,因为page-skeleton-webpack-plugin在编译时,注入代码依赖<!-- shell -->注释,而vue-cli3中集成的html-webpack-plugin会在编译做压缩,将注释去掉,因此要单独配置一下,否则会在编译时导致生成app节点下没有内容。

还有一个在使用时需要注意的点,如果你是vue-cli3脚手架生成的代码,运行时可能会报这样的错误:

如果遇到这个错误,怎么解决呢?github上已经有对应的解决办法了,问题都说完了接下来看看怎么使用,运行项目后,在chrome调试器里执行toggleBar


会在页面里显示一个Preview skeleton page按钮,点击后会生成一个新窗口

这个窗口显示了当前页面的骨架屏样式和代码,可以修改骨架屏样式,然后点击右上角保存,会将对应路由的骨架屏保存到pathname对应的文件夹下

然后执行编译,编译后会在staticDir中生成路由对应的html,这些html中的app节点下都被插入了路由对应骨架屏代码,然后在staticDir下启动服务访问,就能看到骨架屏的效果:

从加载过程中可以看到骨架屏的加载

总结

本篇文章简单介绍了个人在白屏优化实践上尝试过的方案,每个方案都个有自己的优劣,需要自己根据实际的业务场景进行取舍,希望对大家在解决此类问题时有所帮助。

 

===============================

加载优化的意义

    从输入URL到页面展示,发生了什么?
    1、首先从本地查找域名,有的话直接用hosts文件里的ip地址,否则查询DNS,得到ip地址
    2、建立TCP连接——进行“三次握手”
    3、客户端发送http请求
    4、服务端处理,并返回结果给客户端
    5、关闭TCP连接——需要“四次挥手”
    6、浏览器收到结果,开始解析资源(JS、CSS、HTML),解析HTML生成的dom树,和同时解析css生成的cssom树结合生成渲染树
    7、根据渲染树渲染页面

页面的打开速度对网站的优化有极大的意义,那么,如何评价一个页面打开的快不快,可以用两个指标描述:1、ready时间; 2、load时间

这个可以从控制台看到,这里交大家一个方法:
我们可以在控制台输入window.performance.getEntriesByType('paint') 来获取 First Paint (FP:文档中任意元素首次渲染时间)和 First Contentful Paint (FCP:也就是我们常说的 白屏时间 )
我以我所做的某一个网站(优C工作室)为例:
在这里插入图片描述
这两个值也不是固定的,比如在第一次打开页面和第二次打开页面时是不同的,,,
再来看另一个:
在这里插入图片描述
优化的几个方法

减少渲染堵塞
(1)减少head标签的JS堵塞
雅虎军规 里说,所有放在head标签里的css和js都会堵塞渲染。那么,如果这些css和js需要加载和解析很久的话,页面就空白了。(这恐怕是很多“同行”恐惧的事情吧…)
我试着渲染了几个js格式,发现,对于加载时间来说,js的渲染时间几乎可以忽略不计!
解决办法: 1、将script放在body后面
2、给script加 defer 属性(值)
(defer是HTML5新增的属性。一旦script是defer延迟的,那么这个script将会被 异步加载 ,不会马上执行,会在readystatechange变为Interactive后按顺序依次执行,例如:)

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <script src="js/ss1.js" defer></script>
</head>
<body>
<script>
    ......
</script>
</body>
</html>

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

通过谷歌的浏览器插件,我发现,body中script最先执行,然后才是内联的script执行,不过他们可以并行加载。
但是前面说了,defer后的脚本会被异步加载,但是延后执行(最后),其内部发生了重大变化,不能够影响渲染DOM的过程,只能是渲染完成后才能生效。(此时其优先级甚至比图片的优先级还要低) 很多人想要在HTML结构中调用script的话,这样做就有些不妥了。
而且,defer只能在IE9之前浏览器中使用!
还有一个是 async ,这也是个异步加载,它可以加载完就执行,但是 async只能用在加载外部脚本,js不能写在script里 。

所以现在一般不推荐将js写在head里,不管是内联的还是script。

我还是推荐第一种解决办法,这也是目前大多数网站使用的办法。

(2)减少head里的css资源
由于css必须要放在head里(否则页面加载会出问题,比如加载完成后又进行DOM重绘),但是css太多,在head里又会阻塞页面渲染,所以我们要尽可能减少css代码量。(开始社团官网代码量巨多,后来精简、重构了一次,才发现原来不成熟,写的东西有好多漏洞,虽然现在看来和原来差不多,其代码可是很“精要”啊,继续努力!)
我们不能放太多base64在css里面,这样会导致css极度膨胀,例如:把一张3k的图片转成base64,体积将变成4k。
我曾经解决过一个“ 鼠标移入图片变换 ”的问题,这需要放两张图片,通过css的hover伪类达到效果(曾经说过: 能用css解决的就不要用js ),但是我发现第一次hover的时候不会马上变换,要稍微等到图片加载完,这在产品角度上时不可接受的!
一种解决办法就是我在另一篇博文(本文开头处)中提到的 预加载 , 另一种解决办法是:把hover写在svg里面,如下:

<svg>
    <style>
        .a1 { fill: #282828; }
        .a1:hover { fill: #3399cc; }
    </style>
</svg>

    1
    2
    3
    4
    5
    6

这样,如果我们开启了gzip压缩,实际传输大小不过30k。

但是,这种方法依旧不推荐使用,涉及图片,谁也说不准啊。
像一般的图片,有位前辈提点我说,完全可以用图标字体的解决方案。将svg转为icon,世界一片清净,,,
这样,gzip后大小不到10k。

优化图片
这个话题在文首博文中详细介绍,此处不再多说!

压缩和缓存
前面提到gzip,我们发现这能大大减少文件的体积,例如一个180k的css文件被压成30k,就减少了83%的体积。
如何开启,这需要Nginx服务器的支持(关于Nginx,我也多次提到,不再多说):
在 nginx.conf (Nginx的配置)中:

server{
    gzip on;
    gzip_types    text/plain application/javascript application/x-javascript text/javascript text/xml text/css;
}

    1
    2
    3
    4

这属于HTTP协议的内容
但是它有缺点,就是重新渲染的问题
我们不如使用 etag ,Nginx开启etag只需要在server配置( nginx.conf etag )里加上一行:etag on; 即可。

升级到HTTP/2
据说现在http2已经席卷而来,而且其有一个强大的优势,在于对于一个域只进行一次tcp连接,使用多路复用,传输多个资源(同时加载),这样就不必使用诸如雪碧图、合并css/js文件等技术减少请求数了(使用雪碧图只有一个优点:减少请求次数,这和它不可避免的缺点(高清屏会失真、图片变化极不方便)相比,简直不足为道)。
这个技术的使用也很简单,只需要使用nginx 1.10.0和openssl 1.0.2以上版本,安装好后再配置文件中( ngnix.conf )加上:listen 443 ssl http2; 即可。(写在http块中的server块中)
当然,对于不兼容HTTP2的浏览器,nginx也会自动处理。

哦,对了,还要说说 代码优化 :HTML不要嵌套太多层(否则会加重页面layout的压力),css选择器别写的太复杂(不然计算量会很大),js不要滥用闭包(闭包会加深作用域链,加长变量查找时间)

还有,上面提到的nginx的优化,前提是:你用的nginx服务器,否则,你优化他干什么?
开头说了渲染树,这里说下“渲染过程”

    浏览器渲染进程会开启多个线程协作完成!
    1、GUI渲染线程 ,负责渲染浏览器界面,解析HTML、css,构建DOM树和RenderObject树,布局和绘制 ——一旦界面因为某种操作引发了回流,此线程就会执行
    2、JS引擎线程 ——和GUI线程互斥,在js引擎执行时,GUI线程被挂起
    3、事件触发线程 ——依赖js的队列机制完成(当一个事件触发时该线程会把事件添加到待处理队列的队尾,等待js引擎处理)
    4、定时器触发线程 ——依赖js的队列机制完成
    5、异步http请求线程
————————————————
版权声明:本文为CSDN博主「云小梦」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_43624878/java/article/details/97685279

 

posted @ 2020-06-09 11:23  DAVENEE  阅读(755)  评论(0编辑  收藏  举报