首屏加载的意义不言而喻,毕竟第一印象最重要,直接影响用户体验和留存。当用户使用你的产品的时候,一上来半天刷不出首页,很多用户往往就直接给你Ctrl+F4了。
那么问题来了,怎么做首屏优化。在了解怎么优化之前,我们需要知道首屏加载的几个重要时刻。
图片
- 首次加载
- 什么时候加载出页面
- 什么时候用户可以交互
为此,我们可以从以下几个方面来进行相关的优化。
资源体积太大
资源压缩与合并/代码拆分
将小图片内联为Data URL,也可以额减小HTTP的请求数量,需要注意的是,浏览器缓存并不会存储Data URL格式的图片,放在css的background-image属性中即可。由于使用Data URL在渲染和CPU消耗上更大,所以使用时也需要谨慎而不应该一味的滥用。
通过以上几种方法,我们主要要解决的问题是以下几个
- 减少HTTP请求数量
- 减少请求资源的大小
- 减少不必要的代码
需要注意的是,在CSS和JS合并的时候我们需要谨慎,并非所有CSS和JS合并都是好的,不能一味的为了做首屏或者性能优化而引发了其他方面的问题。在有若干个小文件的时候,或者是没有冲突的同模块的文件的时候是可以考虑合并的。但是如果我们把其他不同模块的CSS和JS也合并到了一起,可能会给后续的解析处理和自己的代码维护带来问题,而且JS文件间还可能会出现命名空间的冲突。这些都是无脑的资源合并会带来的问题。
传输压缩
- Gzip 需要注意的是,太小的文件启用Gzip以后可能反而会增大,这是因为压缩前会写入一段映射字典,但是这个是Byte层面的可以忽略不计。
HTTP2.0和HTTP1.1
图片
3630394917-5b229aa26f852_fix732.png
图上简单概括就是:
- HTTP1.1的keep-alive默认开启,而且keep-alive是按顺序请求返回,等到上一个请求返回以后才会进行下一个请求,会有阻塞问题。keep-alive的请求顺序如下:
- 1.请求style.css
- 2.返回style.css
- 3.请求script.js
- 4.返回script.js 如何判断是否开启了keep-alive可以从Response Headers看到,切记是HTTP1.1中的。
图片
- 但是HTTP2.0就不一样了,HTTP2.0的多路复用可以一次性发送多个请求,不一定是按顺序也不需要等待上一个请求返回。这些请求都有唯一标识,所以可以无序。
比较详细的内容在面试点之《HTTP协议与TCP/IP协议》[1]中有。
HTTP缓存
在此之前,我们要熟悉两个概念,强缓存和协商缓存。
图片
强缓存:浏览器直接从本地缓存中取数据,而不用去请求服务器。Expires/Cache-Control(优先级更高,且为通用字段,请求和返回报文中都可以使用)
图片
- Cache-Control常见的属性有很多:
- max-age:单位是秒,意为缓存的持续时间(寿命)。比如Cache-Control:max-age=60;意为60秒内会直接使用该缓存,而max-age<=0时则每次都要发送请求验证缓存是否有修改,改了的话返回200,没改返回304;
- no-store:不使用缓存,直接去服务器请求数据;
- no-cache:不缓存过期资源;客户端会要求缓存服务器不要从缓存拿数据,而是去请求源服务器,源服务器会告诉缓存服务器,使用缓存时先告诉我验证一下。区别在于直接使用缓存是不会请求源服务器的。
- public:表明响应可以被任何对象(发送请求的客户端,代理服务器等等)缓存;
- privite:表明响应只能被单个用户缓存,不能作为共享缓存(即代理服务器不能缓存它);
- Expires:GMT格式的时间字段,意为到期时间。超过此时间以后资源作废。缺点在于是和本地时间比较,本地时间可能存在误差。也可以设置为0或者负数,此时不使用缓存直接从服务器重新请求数据。
协商缓存:浏览器发送请求到服务器,服务器判断是否使用本地缓存。
Etag(响应头中)和If-None-Match(请求头中)【优先级更高】
图片
两者的值都是该资源的唯一标识字符串。第一次请求时,响应头(Response Headers)中会添加一个Etag的字段,再次请求服务器时,会在请求头报文(Request Headers)中添加If-None-Match字段,它的值就是上次响应头(Response Headers)中的Etag值。服务器会比较两个值是否相同。
- 相同: 返回状态码304 Not Modified,直接取用本地缓存即可。
- 不相同: 说明资源内容已经发生了变化,此时接受请求,返回最新的数据,并更新Etag值。Etag中可能会有W/这样的标识,W/(大小写敏感) 表示使用弱验证器。弱验证器很容易生成,但不利于比较。强验证器是比较的理想选择,但很难有效地生成。相同资源的两个弱Etag值可能语义等同,但不是每个字节都相同。
需要特别注意的是,Etag变化并不代表文件内容一定变化,Etag的值取决于服务端的算法。比如Etag的值由最后一次修改时间+内容经过哈希算法而得,但是此时我编辑了内容并没有修改内容,最后一次的修改时间会发现变化,此时的Etag值肯定也会变化,但是内容并没有发生改变
Last-Modified(响应头中)和If-Modified-Since(请求头中)
图片
如图所示,两者的值都是GMT格式的时间字符串,Last-Modified意为最后一次修改文件的时间,其实这个方法和上一种原理一样,只不过一个是用唯一标识字符串来比较,一个是用最后一次修改的时间来比较。也是第一次请求时,响应头(Response Headers)中会添加一个Last-Modified的字段,记录了最后一次文件修改的时间,然后再次请求时,会在请求头(Request Headers)中添加If-Modified-Since字段,值就是上一次响应时响应头中Last-Modified的值。服务器会比较Last-Modefied和Last-Modefied-Since的值。
- 相同: 返回状态码304 Not Modified,直接取用本地缓存即可。
- 不相同: 说明资源内容已经发生了变化,此时接受请求,返回最新的数据,并更新Last-Modified值。
两者的区别
- 当一个文件周期性修改而文件内容并没有修改的时候,Last-Modified还是会从服务器请求新的数据,这并不是我们希望看到的,这个时候Etag能更好的处理这种情况。
- Last-Modified的精准度只有秒,当一秒内修改多次时,这种修改无法判断。 所以Etag的精度和适用性要强于Last-Modefied,两者虽然可以同时使用,但是服务器会优先验证Etag。
首次进入白屏时间过长
骨架屏(vue-content-placeholders)
效果如下图:(图片来源网络,侵删)
图片
我使用的是这个插件vue-content-placeholders[2],其次,骨架屏是需要自己画的,你需要把布局画好做成一个组件,在需要的页面引用,然后等数据请求回来以后隐藏掉再显示正常的页面就可以。通常仅仅在接口请求比较多的页面用到骨架屏,毕竟当你的页面改动的时候骨架屏的页面布局也需要改动,如果每个页面都使用骨架屏未免太浪费开发时间也增加了日后的维护成本。
- root <content-placeholders>(以下三个属性会应用到所有子组件中)
- animated (default: true; type: Boolean):是否开启动画效果
- rounded (default: false; type: Boolean):是否加上边角度,等同于设置border-radius
- centered (default: false; type: Boolean):是否同时垂直+水平居中显示
- <content-placeholders-heading />
- img (default: false; type: Boolean):是否显示图片模块
图片
- <content-placeholders-text />
- lines (default: 4; type: Boolean):显示几条
图片
- <content-placeholders-img />
图片
可以在标签上直接设置class和style
<content-placeholders class='xxx' :animated='true' :rounded='true' :centered='true'>
<content-placeholders-heading class='xxx' style='xxxx' :img="true" />
</content-placeholders>
<content-placeholders style='xxxx' :animated='true' :rounded='true' :centered='true'>
<content-placeholders-text class='xxx' style='xxxx' :lines="3" />
<content-placeholders-img class='xxx' style='xxxx' />
</content-placeholders>
vue文件引入需要的骨架屏组件即可。
<template>
<div>
<Placeholders-Component v-if="isShow"></Placeholders-Component>
<div v-else class="main">
·······
</div>
</div>
</template>
<script>
import PlaceholdersComponent from "./PlaceholdersComponent";//引入写好的骨架屏组件
export default {
components: {
PlaceholdersComponent,
},
data() {
return {
hidden: true,
};
},
};
</script>
懒加载
- Vue的路由懒加载(官方文档[3]) 在我们的项目构建加载时,JavaScript文件才是最影响加载时长的,此时我们把各个路由切分成块,按需加载路由,这样会节约相当多的时间。
使用方法也很简单:
const Foo = () => import('./Foo.vue')
const Bar = () => import('./Bar.vue')
const router = new VueRouter({
routes: [{
path: '/foo',
component: Foo
},{
path: '/bar',
component: Bar
}]
})
- 图片懒加载 在我的《前端性能优化(二):图片[4]》文中已经讲了比较详细的图片懒加载在此就不多解释了。
预渲染
我使用的是Vue.js官网介绍使用的prerender-spa-plugin。这个插件的配置很多,它的GitHub地址在这里[5]。
Vue.js的官网中给到该预渲染插件如下描述:
图片
预渲染也有一些需要注意的点:
- 如果路由过多,成百上千(当然这应该是极少数情况)的时候,使用预渲染会非常缓慢。虽然说每次更新只用执行一次,但是需要的时间会非常长。
- 接口请求过多的时候不建议使用预渲染,而是使用SSR;如果没有SEO的需求,骨架屏是个更简单方便的选择,不需过多的配置更不需要后台配合。
- 预渲染不执行JavaScript,只试用于纯静态页面,当然你也可以配置等接口请求返回拿到数据以后再预渲染,这种仅仅适用于少量接口请求。而SSR比预渲染的不同就是多了一步执行JavaScript。
安装
npm i prerender-spa-plugin -D
webpack.prod.conf.js
const PrerenderSPAPlugin = require('prerender-spa-plugin')
const Renderer = PrerenderSPAPlugin.PuppeteerRenderer
const webpackConfig = merge(baseWebpackConfig, {
plugins:[
new PrerenderSPAPlugin({
// 必需 - 要预渲染的 webpack 输出应用程序的路径。
staticDir: path.join(__dirname, '../dist'),
// 必需 - 要渲染的路由
routes: ['/', '/about'],
renderer: new Renderer({
inject: {
foo: 'bar'
},
// 渲染时显示浏览器窗口。用于调试。false意为打开,true是不打开
headless: false,
// 可选 - 等待渲染,直到在文档上调度指定的事件。
// 例如,使用 `document.dispatchEvent(new Event('event-home'))`
renderAfterDocumentEvent: 'event-home'
})
}),
]
})
main.js
new Vue({
el: '#app',
store,
router,
components: { App },
template: '<App/>',
mounted() {
document.dispatchEvent(new Event('event-home'))
}
})
npm run build以后我们会得到如下的文件,这个时候我们会发现,是多了一个about的文件夹。这是由于配置了new PrerenderSPAPlugin中的routes。
图片
图片
而且此时我们会发现,在打包好的index.html文件中,id为app的div标签中有其他标签内容。倘若我们不使用预渲染,打包出的文件里,这个标签里是空的不会有任何标签。
图片
图片
SSR(服务端渲染)
- 分页首屏加载
- 各好的SEO
- 搜索排名很重要的时候
- 大型架构,动态页面,且面对公众用户
具体使用可参考Vue的官方文档
Service workers
Service workers可以很好的解决离线时候的首页加载问题。但是鉴于文章长度限制,就在此处不细说了。
优化资源加载的顺序
某乎有位大佬说的很详细,链接在此[6]。
prefetch
主要加载较晚才会用到的资源,告知浏览器后,浏览器就会在闲时去加载对应的资源,很适合在懒加载时使用。
<link rel="prefetch" href="xxxxx.css">
对于使用prefetch获取资源,其优先级默认为最低Lowest,可以认为当浏览器空闲的时候才会去获取的资源。
图片
preload
主要用来加载当前页面很重要的资源。
<link rel="preload" href="xxxxx.png" as="image">
浏览器通过as值能得知资源类型,还能根据as的值发送适当的Accept头部信息。甚至可以通过as来标识他们请求资源的优先级(比如说使用as="style"属性将获得最高的优先级,即使资源不是样式文件)
图片
两者的异同
- 如果关闭了浏览器,倘若请求还没有结束,preload会立刻结束请求,但是prefetch会继续请求。
- 如果preload还未下载完就已经开始解析所需资源,此时并不会二次请求,而是等待此次请求完毕。
- 倘若对同一个资源同时使用preload和prefetch则会导致二次加载。
- preload是告诉浏览器页面必定需要的资源,浏览器一定会加载这些资源,而prefetch是告诉浏览器页面可能需要的资源,浏览器不一定会加载这些资源。
补充
关于Vue中的一些优化(并非首屏优化),我推荐黄轶老师的这篇文章《揭秘 Vue.js 九个性能优化技巧》[7]。
在我个人理解看来,性能优化的最终目的并不是完全追求时间上的长短,核心目的是给用户更好的体验,在提升了帧数的情况舍弃一点点加载或者渲染时间在整体体验上要比完全追求数值上的长短有意义的多。也就是上面文章中这位朋友分享的观点:
图片