OTT端性能优化建设之本地缓存设计 | 《优酷OTT互联网大屏前端技术实践》第七章

简介: 目前,做2C业务的应用,更多强调SSR、客户端缓存以及PWA等,以实现首屏加载体验优化、秒开等性能指标,相比较而言,这些策略更加“综合”“强壮”,如果合理运用以及借助端能力,实现冷启动提速、首屏加载优化、秒开等不在话下。但是笔者业务服务于“OTT端酷喵APP”前端业务,主要是酷喵APP的HTML5投放(目前更换使用Rax),而端内浏览器并不支持service worker(PWA),且受制于端及浏览器内核,并无zcache类似能力。至此,大写的无奈涌上心头,这种情况还能不能抢救一把?答案是:可以,localStorage迂回包抄方案。也介于此,本文方案诞生,虽不完美,但是终究有闪光所在。

上一章:不一样的烟火:记OTT端半屏互动能力建设 | 《优酷OTT互联网大屏前端技术实践》第六章>>>

点击免费下载
《优酷OTT互联网大屏前端技术实践》>>>

test

作者| 阿里巴巴文娱技术 魏家鲁

一、背景

目前,做2C业务的应用,更多强调SSR、客户端缓存以及PWA等,以实现首屏加载体验优化、秒开等性能指标,相比较而言,这些策略更加“综合”“强壮”,如果合理运用以及借助端能力,实现冷启动提速、首屏加载优化、秒开等不在话下。

但是笔者业务服务于“OTT端酷喵APP”前端业务,主要是酷喵APP的HTML5投放(目前更换使用Rax),而端内浏览器并不支持service worker(PWA),且受制于端及浏览器内核,并无zcache类似能力。至此,大写的无奈涌上心头,这种情况还能不能抢救一把?答案是:可以,localStorage迂回包抄方案。也介于此,本文方案诞生,虽不完美,但是终究有闪光所在。

二、方案

在localStorage出现之前,浏览器层面可用的本地存储只有一个:cookie。作为前端本地存储的独苗,cookie在很长一段岁月里扮演了极其重要的角色,如账号数据存储、状态标记等等。然而,4kb的容量让cookie在资源缓存道路上无力前行。随着HTML5的兴起,一个崭新的名词出现“localStorage”,容量比cookie大千倍、同步读写的特点,一经面世就被认定为实用型本地存储策略,因而被广大开发者关注。

三、优势与局限

1、localStorage的优势

1) localStorage拓展了cookie的4K限制,5mb(各浏览器略有不同);
2) 键值对格式,同步读写,使用简单;
3) 持久存储,不随请求发出,不影响带宽资源。

2、localStorage的局限

1) 浏览器支持不统一,比如IE8以上的IE版本才支持localStorage这个属性;
2) 目前所有的浏览器中都会把localStorage的值类型限定为string类型,这个在对我们日常比较常见的JSON对象类型需要一些转换;
3) localStorage本质上是对字符串的读取,大量读写影响浏览器性能。

兼容情况

image.png

本地缓存分析

首先5MB的容量,对于存储前端部分常用的js、css等,如果经过系统逻辑筛选存储,虽然不从容,但也算能用,这也是localStorage可以作为前端本地缓存的第一要素。当然这5MB的容积,“大”是相对于cookie而言,真正存储资源则需要筛选和过滤的。比如一个网页中,包含大量js、css、img、font文件,这个量级是不可预估的,如果选择全部存储,5MB空间可能瞬间爆满。一般而言,我们更倾向于存储js、css这种资源(尤其是阻塞式加载的js资源)。

其次,localStorage只能存储字符串类型数据,这就决定了我们无法使用< script src="static/a.js">的形式加载js资源,这种情况下,我们无法拿到“文件句柄(字符)”,同时也无法赋予其本地缓存的资源。如果要拿到js文件句柄则需要转换思路,使用xhr或则fetch的形式得到文件字符。

再次,得到字符后,我们可以选择将其存储至localStorage的同时,进行eval或者new Function,使js代码执行。

最后,当页面再次打开,我们可以根据js路径情况,判断本地是否有缓存资源,如果有,则取出并且eval/new Function,使代码执行;如果没有,则使用xhr/fetch进行请求,文件资源返回后,存储至localStorage并执行eval/new Function。
至此,整个缓存逻辑即告成功。下面我们以实际代码形式进行技术方向解析

四、缓存技术实现

客户端本地请求/缓存/加载器

1、localStorage api

设置localStorage 项,如下:
localStorage.setItem('keyName', 'keyValue');
读取 localStorage 项,如下:
localStorage.getItem('keyName');
移除 localStorage 项,如下:
localStorage.removeItem('keyName');
移除所有localStorage 项,如下:
localStorage.clear();

2、实现原理图:

image.png

3、主要代码实现:

(function () {
  constoHead=document.getElementsByTagName('head')[0];
  const_localStorage=window.localStorage|| {};
  //创建ajax函数letajax=function (_options) {
    constoptions=_options|| {};
    options.type= (options.type||'GET').toUpperCase();
    options.dataType=options.dataType||'javascript';
    constparams=options.data?formatParams(options.data) : '';
    //创建-第一步letxhr;
    if (window.XMLHttpRequest) {
      xhr=newXMLHttpRequest();
    } else {
      xhr=ActiveXObject('Microsoft.XMLHTTP');
    }
    //在响应成功前设置一个定时器(响应超时提示)consttimer=setTimeout(function () {
      //让后续的函数停止执行xhr.onreadystatechange=null;
      console.log('timeout:'+options.url);
      options.error&&options.error(status);
    }, options.timeout||8000);
    //接收-第三步xhr.onreadystatechange=function () {
      if (xhr.readyState==4) {
        clearTimeout(timer);
        conststatus=xhr.status;
        if (status>=200&&status<300) {
          options.success&&options.success(xhr.responseText, xhr.responseXML);
        } else {
          options.error&&options.error(status);
        }
      }
    }
    //连接和发送-第二步if (options.type=='GET') {
      xhr.open('GET', options.url+'?'+params, true);
      //xhr.setRequestHeader("Accept-Encoding", "gzip");xhr.send(null);
    } elseif (options.type=='POST') {
      xhr.open('POST', options.url, true);
      //设置表单提交时的内容类型xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
      xhr.send(params);
    }
  }
  //格式化参数letformatParams=function (data) {
    letarr= [];
    for (letnameindata) {
      arr.push(encodeURIComponent(name) +'='+encodeURIComponent(data[name]));
    }
    //arr.push(('v=' + Math.random()).replace('.',''));returnarr.join('&');
  }
  //时间戳转日期格式  letformatDateTime=function (time, format) {
    constt= (time&&newDate(time)) ||newDate();
    lettf=function (i) {
      return (i<10?'0' : '') +i
    };
    returnformat.replace(/YYYY|MM|DD|hh|mm|ss/g, function (a) {
      switch (a) {
        case'YYYY':
          returntf(t.getFullYear());
          break;
        case'MM':
          returntf(t.getMonth() +1);
          break;
        case'DD':
          returntf(t.getDate());
          break;
        case'hh':
          returntf(t.getHours());
          break;
        case'mm':
          returntf(t.getMinutes());
          break;
        case'ss':
          returntf(t.getSeconds());
          break;
      }
    })
  };
  lethandleError=function (url, callback, _send) {
    letscript=document.createElement('script');
    script.type='text/javascript';
    script.onload=script.onreadystatechange=function () {
      if (!this.readyState||this.readyState==="loaded"||this.readyState==="complete") {
        console.log('create script loaded : '+url);
        callback&&callback();
        _send&&_send();
        script.onload=script.onreadystatechange=null;
      }
    };
    script.onerror=function () {
      console.log('create script error : '+url);
      _send&&_send();
      script.onload=null;
    }
    script.src=url;
    oHead.appendChild(script);
  }
  //eval js字符串代码let_eval=function (fnString) {
    window['eval'].call(window, fnString);
  }
  letautoDel=function () {
    if (!_localStorage.setItem) {
      return;
    }
    for (letkeyin_localStorage) {
      //console.log('第'+ (i+1) +'条数据的键值为:' + _localStorage.key(i) +',数据为:' + _localStorage.getItem(_localStorage.key(i)));//let key = _localStorage.key(i);constvalue=key.indexOf('##') >-1?_localStorage.getItem(key) : '';
      constisSetExpire=value.split('##').length>1;
      constdate=isSetExpire&&value.split('##')[1];
      constisExpire=date&&nowDateNum>+date.replace(/-/ig, '');
      if (isExpire) {
        _localStorage.removeItem(key);
        _localStorage.removeItem(key+'_data');
        console.log('DEL:'+key+'|'+key+'_data');
      }
    }
  }
  letonIndex=-1;
  letsend=function (list, index) {
    constnum=list.length;
    if (!num||num<index+1) {
      autoDel();
      return;
    }
    if (index<=onIndex) {
      return;
    }
    onIndex=index;
    constitem_url=list[index].url;
    constitem_aliases=list[index].aliases;
    constcallback=list[index].callback;
    constisStorage=list[index].storage!==false ;
    // if (!_localStorage) {//   handleError(item_url, callback);//   send(list, index + 1);//   return;// }constisDone=item_url=== (_localStorage.getItem&&_localStorage.getItem(item_aliases));
    if (isDone) {
      constfnString=_localStorage.getItem(item_aliases+'_data');
      try {
        _eval(fnString);
      } catch (e) {
        console.log('eval error');
      }
      callback&&callback();
      console.log('local:'+item_aliases);
      send(list, index+1);
      return;
    }
    ajax({
      url: item_url,
      success: function (response, xml) {
        //请求成功后执行try {
          _eval(response);
        } catch (e) {
          console.log('eval error');
        }
        //window['eval'].call(window, response);// ( window.execScript || function( script ) {//     window[ 'eval' ].call( window, script );// } )( response );callback&&callback();
        console.log('ajax:'+item_aliases);
        constisSetExpire=item_url.split('##').length>1;
        constdate=isSetExpire&&item_url.split('##')[1];
        constisExpire=date&&nowDateNum>+date.replace(/-/ig, '');
        if (isStorage&&_localStorage.setItem&&!isExpire) {
          try {
            _localStorage.setItem(item_aliases, item_url);
            _localStorage.setItem(item_aliases+'_data', response);
          } catch (oException) {
            if (oException.name=='QuotaExceededError') {
              console.log('超出本地存储限额!');
              _localStorage.clear();
              _localStorage.setItem(item_aliases, item_url);
              _localStorage.setItem(item_aliases+'_data', response);
            }
          }
        }
        send(list, index+1);
      },
      error: function (status) {
        //失败后执行console.log('ajax '+item_aliases+' error');
        handleError(item_url.replace('2580', ''), callback, function () {
          send(list, index+1);
        });
        setTimeout(function () {
          send(list, index+1);
        }, 300)
      }
    });
  }
  constnowDateNum=+formatDateTime(false, 'YYYYMMDD');
  send(window.__page_static, 0);
})();
//一期实现:localStorage缓存/存储;//后续加强:PWA/indexeddb(待定);//后续加强:引入前端静态资源版本diff算法,局部更新本地版本;

4、使用方法:

• 4.1-配置静态资源别名
window.__page_static= [
    {    
        aliases: 'FocusEngine',
        url:'//g.alicdn.com/de/focus-engine/2.0.20/FocusEngine.min.js'
    },
    {
        aliases: 'alitv-h5-system',
        url: '//g.alicdn.com/de/alitv-h5-system/1.4.9/page/main/index-min.js',
        callback: ()=>{ window.Page.init() );
    }
]
• 4.2-引入种子文件:
<script charset="utf-8" src="//g.alicdn.com/de/local_cache/0.0.1/page/localStorage/index-min.js"></script>
• 4.3、过期设置:
window.__page_static= [
    {    
        aliases: 'FocusEngine',
        url:'//g.alicdn.com/de/focus-engine/2.0.20/FocusEngine.min.js##2018-08-10'
    },
    {
        aliases: 'alitv-h5-system',
        url: '//g.alicdn.com/de/alitv-h5-system/1.4.9/page/main/index-min.js##2018-08-30',
        callback: ()=>{ window.Page.init() );
    }
]

tips:

每次加载TVcache后,TVcache通过xhr方式请求得到js资源,并自动根据“##”分割得到有效期:
1、当无“##日期”时,则默认长期缓存;
2、当“##日期”大于请求日期时,则请求会资源后,正常存入local;
3、当“##日期”小于请求日期时,则请求会资源后,不存入local,并在所有请求结束后,检索local,删除之;

• 4.4、禁用本地缓存:
window.__page_static= [
    {    
        aliases: 'FocusEngine',
        url: '//g.alicdn.com/de/focus-engine/2.0.20/FocusEngine.min.js##2018-08-10',
        storage: false
    },
    {
        aliases: 'alitv-h5-system',
        url: '//g.alicdn.com/de/alitv-h5-system/1.4.9/page/main/index-min.js##2018-08-30',
        storage: falsecallback: ()=>{ window.Page.init() };
    }
]
• 4.5、删除本地缓存:

方法1:同别名文件,修改文件版本号如
前期布设:

window.__page_static= [
    {
        aliases: 'alitv-h5-system',
        url: '//g.alicdn.com/de/alitv-h5-system/1.4.9/page/main/index-min.js'
    }
]

更新布设:

window.__page_static= [
    {
        aliases: 'alitv-h5-system',
        url: '//g.alicdn.com/de/alitv-h5-system/1.4.10/page/main/index-min.js',
    }
]

方法2:手动设置删除数组 ( 不推荐 )

window.__page_static_del= ['alitv-h5-system']

业务应用
OTT端酷喵APP-H5投放页

五、总结

我们上文提到,该方案将资源文件缓存到本地,之后项目页面再次请求时与本地版本号比对,之后进行决定是用本地缓存还是重新fetch。而在笔者所从事的OTT端HTML5业务,是将“焦点引擎文件”、“框架文件”、“公共组件文件”默认存储于本地,业务文件视情况配置。

经过该部署方案的投放,我们监测显示,冷启动平均提速30%,尤其在弱网情况下更加明显,而相应使load超时情况大大降低。

然而,技术总是在进步的,近一两年阿里集团内2C业务,H5逐渐被Weex/Rax技术栈取代,相应的在OTT端H5缓存的技术探索也失去了业务依托,当然这并不是结束。换句话说,这是新的探索的开始,在OTT端Rax能力创建建设中,我们也开启了新的缓存方案建设,并且取得了一定的成绩,后续笔者将继续行文介绍,Rax在OTT端的缓存建设。

相关文章
|
6月前
|
存储 缓存 算法
【C/C++ 性能优化】提高C++程序的缓存命中率以优化性能
【C/C++ 性能优化】提高C++程序的缓存命中率以优化性能
970 0
|
16天前
|
存储 缓存 监控
|
6月前
|
存储 缓存 NoSQL
Redis多级缓存指南:从前端到后端全方位优化!
本文探讨了现代互联网应用中,多级缓存的重要性,特别是Redis在缓存中间件的角色。多级缓存能提升数据访问速度、系统稳定性和可扩展性,减少数据库压力,并允许灵活的缓存策略。浏览器本地内存缓存和磁盘缓存分别优化了短期数据和静态资源的存储,而服务端本地内存缓存和网络内存缓存(如Redis)则提供了高速访问和分布式系统的解决方案。服务器本地磁盘缓存因I/O性能瓶颈和复杂管理而不推荐用于缓存,强调了内存和网络缓存的优越性。
649 47
|
6月前
|
存储 缓存 自然语言处理
深入PHP内核:理解Opcode缓存与性能优化
【5月更文挑战第14天】 在动态语言的世界里,PHP一直因其高性能的执行效率和广泛的社区支持而备受青睐。随着Web应用的复杂性增加,对性能的要求也越来越高。本文将探讨PHP的Opcode缓存机制,解析其对性能提升的贡献,并展示如何通过配置和使用不同的Opcode缓存方案来进一步优化PHP应用的性能。我们将深入到PHP的核心,了解Opcode是如何生成的,以及它如何影响最终的执行效率。
|
3月前
|
缓存 前端开发 Linux
哇塞!NPM 缓存竟成开发拦路虎?快来掌握清空秘籍,开启前端开发逆袭之旅!
【8月更文挑战第20天】NPM是前端开发中管理依赖的关键工具。有时需清空其缓存以解决版本不一致或包损坏等问题,确保使用最新依赖。可通过命令`npm cache clean --force`强制清空全部缓存,或手动删除各系统下的缓存文件夹。注意清空缓存可能延长后续安装时间,建议事先备份依赖或确保可重新安装。正确管理缓存有助于提升开发效率。
74 1
|
3月前
|
缓存 JavaScript 前端开发
【性能革命!】Vue 3事件监听缓存的奥秘 —— 揭开前端优化的神秘面纱,让应用性能飙升的秘密武器!
【8月更文挑战第7天】随着前端应用日益复杂,性能优化变得至关重要。Vue 3 通过引入事件监听缓存等新特性提升了应用性能。此特性避免了重复注册相同的事件监听器,减少了资源浪费和潜在的内存泄漏问题。在 Vue 3 中,事件监听器首次渲染时注册,并在后续渲染中重用,除非组件状态变更或手动更新。通过一个示例组件展示了如何利用该特性优化性能,包括使用 `watchEffect` 或 `watch` 在状态变化时重新注册监听器。这一机制降低了浏览器负担,减少了内存占用,提高了应用响应速度,尤其对大型应用效果显著。合理运用事件监听缓存能够构建出更加流畅的应用体验。
293 3
|
4月前
|
存储 缓存 前端开发
(三)Nginx一网打尽:动静分离、压缩、缓存、黑白名单、跨域、高可用、性能优化...想要的这都有!
早期的业务都是基于单体节点部署,由于前期访问流量不大,因此单体结构也可满足需求,但随着业务增长,流量也越来越大,那么最终单台服务器受到的访问压力也会逐步增高。时间一长,单台服务器性能无法跟上业务增长,就会造成线上频繁宕机的现象发生,最终导致系统瘫痪无法继续处理用户的请求。
139 1
|
4月前
|
缓存 算法 前端开发
前端 JS 经典:LRU 缓存算法
前端 JS 经典:LRU 缓存算法
94 0
|
5月前
|
存储 缓存 JavaScript
【前端 - Vue】之 Keep-Alive缓存组件使用语法及原理解析,超详细!
【前端 - Vue】之 Keep-Alive缓存组件使用语法及原理解析,超详细!
|
6月前
|
存储 缓存 自然语言处理
深入PHP内核:Opcode缓存与性能优化
【5月更文挑战第31天】 在提升PHP应用性能的众多策略中,Opcode缓存技术以其显著的性能提升效果而广受关注。本文旨在深入探讨PHP的Opcode缓存机制,解析其对性能提升的影响,并讨论如何通过配置和优化实践来充分利用Opcode缓存。文章将首先介绍Opcode的概念及其在PHP执行过程中的作用,然后分析几种流行的Opcode缓存解决方案,最后提供针对性的优化建议,帮助开发者实现高效的PHP应用。

热门文章

最新文章