优化页面的打开速度,要不要了解一下~

11,109 阅读11分钟

对于一个网站来说打开速度是一个很重要的指标,只是大部分时间内我们的精力可能都用来对付需求了,特别是当我们做的是一些内部的项目时,我们常常的会忽略了这一方面的优化。其实要对一个页面的打开速度做出一些比较常见的优化并没有想象中的困难,本文将带你做一些既不费力也不费时间的优化操作,这些操作中涉及到压缩,缓存,preload加载关键资源,prefetch缓存懒加载资源与一些引用组件的建议及常见的工具库处理。

开启gzip压缩

当我们使用webpack打包并压缩js代码后,往往某些js(比如vendor)依然会很大,可能会达到1mb左右的大小,虽然以现在的带宽来说如果我们是pc端项目这也不是什么大问题,但是这里面明显是存在着相当大的优化空间的,gzip就是一种形式。 在浏览器的请求头里包含着这样一句话Accept-Encoding:gzip, deflate,这告诉我们浏览器是可以识别gzip压缩的,使用gzip压缩后的文件将大大减小,很多情况下甚至能压缩70%。现在的服务基本上都是使用nginx做转发的,对于我们来说开启gzip其实相当容易,只要配置如下的代码就可以了。

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

no-gzip
gzip

上图能看到没开启gzip时vendor近800k,而开启gzip后大概只有250k。

可以说如果我们连gzip都没有开启的话,其他任何优化都显得有点多余,因为大概没有另外一种优化方式能压缩如此高的比例。

浏览器缓存

除了常见的gzip压缩外,另一个可以利用的优化点就是浏览器的缓存。我会顺带着介绍一下浏览器的缓存方式,但是不会过于详细,有兴趣的同学可以额外去找一些资料学习。

浏览器分为两种缓存,强缓存与协商缓存(也被称为弱缓存),其中协商缓存不用我们自己配置,下面我们通过连续两次刷新页面来观察一下协商缓存。

如上图,在第一次的请求中nginx的http响应头中携带了一个Last-Modified: sometime
那么第二次的请求中浏览器的请求头里就会携带这个时间去对比,当nginx的时间在这个时间之前那么就说明当前资源并没有产生变化,返回的状态码也会对应的变成304, 当浏览器收到304后说明缓存的资源并没有过期,浏览器就会去读取已经缓存好的文件。需要注意的是虽然浏览器最终是调用的缓存,但是仍然存在http请求来确认该缓存是否失效,所以很明显还有另外的一种方式让浏览器可以直接调用缓存,不需要通过http请求,这就是强缓存。强缓存在nginx中可以通过如下代码配置

location ~* \.(css|js)$ {
    proxy_set_header        Host  $host;
    proxy_pass              http://tomcat_xxx;
    expires                 7d;
}
location ~* \.(jpg|jpeg|png|gif|webp)$ {
    proxy_set_header        Host  $host;
    proxy_pass              http://tomcat_xxx;
    expires                 30d;
}

注意我们这里使用的是tomcat,你可能需要的配置与我这个并不一样,但是这并不关键,我们主要需要的是expires这项配置,他表示了我们希望缓存的时间,我们配置的js与css缓存时间为10天,而图片则缓存30天。一起打开浏览器看一下效果。

max-age
这里浏览器响应头中会附带一个max-age=604800,这里的单位是秒,换算成天就是我们刚才设置的7天,再次刷新浏览器后状态码依然是200但是后面多了一个from memory cache表明此时是从内存中直接取出缓存,并没有发送http请求,这对一些图片与我们的依赖包vendor相当有用,我们完全可以给这两个资源设定一个较大的缓存时间,这样当用户访问第一次后,这些资源始终会保持在用户的缓存中,就算我们之后更改了很多我们的业务代码,只要依赖没有更改,用户只用加载一些小的业务代码文件就可以了,对于较大的vendor则依然可以从缓存中获取。

我们可以简要的总结一下浏览器的缓存方式并增加一些注意的点。浏览器会首先检测强缓存,如果命中则直接返回缓存文件,不会发送http请求,如果没有命中则去检查弱缓存,当弱缓存命中时返回304状态码,浏览器依然从缓存中获取资源,如果弱缓存也没有命中则返回200状态码重新加载服务器上的资源。

注意点:

  1. 强缓存、弱缓存只是名字上的区别并没有什么强弱之分,其实对于一般的浏览器来说刷新就会使你当前请求资源的强缓存失效,因为刷新的时候会请求头中会携带一个max-age=0或是no-cache,注意我这里说的当前请求资源指的一般是你页面的html文档,但是对于文档中外链的js与img等,不会因为刷新导致强缓存失效。不过如果你直接请求的是一个js文件,那么刷新后这个js文件强缓存也会失效。
  2. 既然强缓存不会发起http请求,那么服务器资源有变更的情况怎么办。其实webpack生成的hash码就是帮我们解决这个问题用的,当外链的app.123456.js变成了app.654321.js浏览器自然会重新发起请求,这也提示了我们尽量不要去改变vendor导致vendor的hash变更产生缓存失效的问题。

对于关键资源的优先加载与一些懒加载资源的预加载

由于我们的技术栈是vue,所以以下示例我们用vue来进行演示,但是本质上无论是什么技术栈都是一样的。 假设我们的项目是单页面应用那么首先应该优化的点就是路由的懒加载,也就是说不要一次性的将所有代码一起返回,只有切换到当前路由时我们才去请求当前路由对应的代码。对于vue-cli初始化的项目来说配置十分的简单,在router中更改一下import的方式就可以。

const router = new Router({
  routes: [
    {
      path: '/',
      redirect: '/a',
    },
    {
      path: '/a',
      component: () => import('../components/a/index.vue'),
    },
    {
      path: '/b',
      component: () => import('../components/b/index.vue'),
    },
]

现在我们就可以根据我们访问的router动态的加载js文件了。但是这样其实还有优化的空间,假设我们现在请求路由a,加载了vendor等公共js与a本身的js,那么在访问a页面的空余时间里为什么我们不将b路由的js也对应的加载到浏览器的缓存中那,这样当用户切换到b路由时就可以不用在发送http请求而是直接使用缓存中的文件就可以了。

在这里我们要用到一个webpack插件,PreloadWebpackPlugin,这个插件的作用是帮助我们对应的生成<link rel="preload" href="xxxx"><link rel="prefetch" href="xxxx">标签,其中preload中href的资源浏览器会优先的进行加载,关于preload的作用mdn文档是如此说的。

在浏览器的主渲染机制介入前就进行预加载。这一机制使得资源可以更早的得到加载并可用,且更不易阻塞页面的初步渲染,进而提升性能。

具体相关其实就是浏览器的关键路径的知识,这里不详述,可以另找资料。

而对于prefetch的href浏览器会进行预加载,同样这里引用mdn文档中的话对其描述

其利用浏览器空闲时间来下载或预取用户在不久的将来可能访问的文档。网页向浏览器提供一组预取提示,并在浏览器完成当前页面的加载后开始静默地拉取指定的文档并将其存储在缓存中。当用户访问其中一个预取文档时,便可以快速的从浏览器缓存中得到。

所以对于vue-cli生成的项目要做的是用preload加载vendor、manifest与app三个js而用prefetch去加载所有路由对应的文件。这样当我们访问路由a时会首先下载需要的js与css,然后浏览器会自动的加载其他的路由文件。此时当用户去访问其他路由时就不会点击时才去发送请求。在webpack.prod.conf.js中加入如下代码,注意放在new HtmlWebpackPlugin()的下面,由于我们的项目中只是用js与css组成的,你可以自己配置img与font这类资源。

new PreloadWebpackPlugin({
  rel: 'prefetch',
}),
new PreloadWebpackPlugin({
  rel: 'preload',
  as(entry) {
    if (/\.css$/.test(entry)) return 'style'
    return 'script';
  },
  include: ['app', 'vendor', 'manifest']
})

关于全局组件的注册

很多第三方组件中让我们使用的方式是在main.js中引入组件,然后通过Vue.component()来注册全局组件,其实很多情况下我们不应该采用这种方案,因为这会增大vendor的体积,特别是当此组件只是在其中的一个路由中用到,放入vendor中就更是不合理的,因为缓存这个组件的代码对我们并没有很多好处,假设页面有5个路由,相当于进其他4个路由时这些代码都是没有意义的,所以很多情况下局部注册组件将其打包进到路由代码中是更好的选择。额外提一句,如果是echarts这种巨型库,还是建议打包进vendor中的,因为由于业务代码总是在变更的,所以路由代码的hash值总是在变化,echarts这种重量的代码就不要每次上线都让用户重新加载一遍了...

关于这一点我再思考之后觉得我可能是错的,因为像引入的第三方组件我们是作为外部依赖来使用的,就算是体积较小,每次打包进路由对应的js里也会因为业务代码的变动导致用户重新加载这部分代码,而体积大的时候就像我上文说的更不能打包进业务代码中。我能想到的场景只有一个情况可能有些不一样,就是当代码上线后我们开发新的需求,需要引入新的依赖,但我们不希望用户缓存的vendor失效,所以我们可以打包一个新的依赖包出来,但这样就提高了http的请求数量,我们知道在http1中我们优化的方向是域名分散与降低请求数,这样到底好不好见仁见智吧,反正我应该是不会这么做的。

一些工具库可能需要我们做一些额外的处理

我们常见的工具库比如lodash与moment其实都相当相当的大,而往往我们只是用几个小功能而已,为了用这些小功能引入这么巨大的库真的有些浪费,这里提一些基础的解决方案。

lodash:安装lodash-webpack-plugin babel-plugin-lodash 在.babelrc中配置"plugins": ["lodash"],去按需引入lodash,但是说实话,按需引入有时候也不小,如果真的用到几个很简单的功能自己写未必不是一个更好的选择。

moment: 用day.js去替换。仅2kb大小的库,与moment一样的api,简直不能再赞,我没看源码,不知道2kb是怎么做到与moment一样的功能的,难道是moment实现的太笨了吗...有点费解

还有类似echarts这种巨型的库也有按需引入的方式,大家可以自己试着优化自己vendor的大小

最后,感谢阅读,如果顺手帮我给原文点个star就更好了,毕竟你也知道这年头star都能换钱~么么哒~