在我们考虑提高页面渲染速度之前先来思考一个问题,一个页面的速度由什么决定?
显而易见,这里主要包含两方面的影响因素。
- 资源传输时间(tcp链接时间和响应时间)
- dom渲染时间
这两方面的耗时可以通过浏览器环境下通过window的内置对象window.performance拿到相关数据并计算得到
const { timing } = window.performance
// 计算资源传输耗时
const tcpTimes = (timing.connectEnd - timing.connectStart)/1000
// 计算dom渲染耗时
const domTimes = (timing.domComplete - timing.domLoading)/1000
复制代码
前端常见的优化方式
所以进行前端优化的方式,通常是针对围绕两个方面进行优化,即尽可能的减少资源传输时间和dom渲染时间。
以下是前端常见的三种优化方式:
- 服务器优化
- 减少传输体积-压缩和切片技术
- 合理利用缓存
展开来讲每一种优化方式都能对应跟多具体的行为,对于服务器优化,vue和react生态体系下都给出了较为成熟的解决方案。比如Vue提出的Nuxt.js和React提出的Next.js。
对于第二种方式,通过减少传输体积来达到优化的目的可以采用一下几种方法:
- 资源压缩(包括图片、字体库以及js脚本)
- Tree shaking技术:剔除无用代码
- 代码分离以及提取公共代码
- 懒加载
- 使用webp格式图片
- 在生产环境关闭source map
- ...
对于第三种方式,即通过合理利用缓存的方式来达到性能优化的目的,这是本文侧重讨论的重点。
缓存方式有哪些
- cookies-缓存量,不安全
- 全局状态缓存-不持久, 占用内存
- 本地缓存-locaStorage,sessionStroage,持久化
对于本地缓存,我们需要知道locaStorage,sessionStroage有哪些区别:
- locaStorage,除非手动删除,不然会一直存在,
- sessionStroage,基于会话级别的缓存策略,页面会话关闭,则清除。
做一个缓存架构
这里我们基于请求发送实现一个缓存架构。在运用缓存进行性能优化时需要考虑的问题有:
- 缓存的量引起的压力,储存压力。
- 缓存的更新问题-如何在数据更新后保持最新数据。
基于这两点的考虑,在对数据进行缓存的时候,数据的变动越少越好。
解决跨域
1、webpack 代理转发:做了一个node中间层——只对开发环境有效
2、nginx代理或者手动做node中间层--成本太大
3、jsonp,适合静态页面和jquery--不适合vue
4、后端接口配置跨域资源共享(CORS)--简单高效
缓存思路:
第一次加载后,后续无需请求
热门商品放入localStroage,非热门的放到状态缓存里
// 缓存对象的创建应该使用单例模式——全局只能有一个缓存对象。
if(!window.mycache){
window.mycache = function(){
window.cache = {};
window.cachArr = [];
return function(){
get:function(api){
return new Promise((resolve, reject) => {
if(cache[api]){
resolve(cache[api])
}else{
if(localStroage.getItem(api)){
resolve(JSON.parse(localStroage.getItem(api)));
}else {
this.set(api).then(res=>{
var name = '';
if(res.data.ishot){
if(cacheArr[1].length>4){
name=cacheArr[1].shift();
localStroage.removeItem(name)
}
localStroage.setItem(api, Json.stringify(res));
cacheArr[1].push(api)
}else {
if(cacheArr[0].length>3){
name = cahceArr[0].shift();
delete cache[name];
}
cache[api] = res;
cacheArr[0].push(api);
}
resolve(res);
})
}
}
})
},
set:function(api){
return axios.get(api);
},
remove:function(api){
delete cache(api);
}
}
}
}
复制代码
这段代码中设计到大量的条件判断语句。总结起来缓存入口可以分为三类:
- 缓存
- localStroage
- 发送请求,处理缓存逻辑
我们可以利用享元模式优化这一流程,享元模式是⼀种经典的结构型解决⽅案,⽤于优化重复、缓慢及数据共享效率较低的代码。它旨在通过与相关的对象共享尽可能多的数据来减少应⽤程序中内存的使⽤。
if(!window.mycache){
window.mycache = function(){
window.cache = {};
window.cacheArr = [];
return {
get:function ( api ) {
var state = 0;
var stateHandler = [
function(resolve,reject) {
resolve(cache[api]);
},
function(resolve,reject) {
resolve(JSON.parse(localStorage.getItem(api)));
},
function(resolve,reject) {
this.set(api).then((res) => {
var type = 0;
var name = cacheArr[type].shift();
if(res.data.ishot){
type == 1;
name = cacheArr[type].shift();
if(cacheArr[1].length>4){
localStorage.removeItem(name);
}
localStorage.setItem(api,JSON.stringify(res));
} else {
if(cacheArr[0].length>3){
delete cache[name];
}
cache[api] = res;
}
cacheArr[type].push(api);
resolve(res);
})
}
]
return new Promise((resolve,reject)=>{
if(localStorage.getItem(api)){
state = 1;
}
if(!cache[api] && !localStorage.getItem(api)){
state = 2;
}
stateHandler[state].call(this, resolve, reject);
})
},
set:function(api){
return axios.get(api);
},
remove:function(api){
delete cache(api);
}
}
}
}
复制代码
基于此,我们已经完成了一个缓存架构的实现,现在可以直接在项目中引入并使用这个缓存架构。
新建.vue文件
<template>
...
</template>
<script>
export default {
date() {
return {
goodInfo: {}
}
},
mounted: function(){
mycahce.get('/api/'+this.$route.params.goodApi).then(res=>{
this.goodsInfo = res.data
})
}
}
</script>
复制代码
现在运行这段代码时候会进入到缓存架构里,先判断localStroage下或者全局状态缓存cache下有没有对应api的值,如果有则直接取出,如果没有则发送axios请求,并缓存对应的数据内容。
在上面的讨论中我们注意到了使用缓存提高性能的同时需要考虑到缓存的量引起的储存压力,在代码里我们设置了最大缓存的量,超过这个量时需要将旧数据删除,存入新数据,这样就能控制最大能缓存的数据的量。
关于第二点考虑,缓存的更新问题,如何保证缓存与实际资源一致的同时,提高缓存命中率。如果时静态资源缓存的话,最好的方式时通过hash值来达到这个目的。但是基于本文提高的针对api请求接口缓存暂时没有很好的解决方案,即便如此,仍然可以通过以下几种方式来达到缓存更新这一目的:
- 你缓存的接口变动的越少越少,可以设置缓存时限以及设定触发删除的动作:比如退出登陆,手动触发缓存删除。这种方式的成本较小,同时也适用与很多前端应用场景,如菜单信息以及个人信息接口等数据可以使用这种缓存策略。
- websocket, 成本比较大,需要后段启动一个websocket服务,能够达到较即时的数据更新。
var ws = new Websocket();
ws.onmessage=(data)=> {
if(data.api){
mycache.remove(api);
}
}
复制代码
- 小成本请求:进入项目时,先发一个小请求,响应只会给你回传改变的接口。关键的设计在于传输的数据量定小,接口需要传入当前抓取数据的时间节点。接口拿到这个时间节点后,对比当前的数据是否更新,如果更新的话返回更新了数据的api。