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

在线体验各类最新模型,更有模型 免费Token 额度领取!
立即体验
简介: 目前,做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端的缓存建设。

相关文章
|
存储 缓存 NoSQL
Redis多级缓存指南:从前端到后端全方位优化!
本文探讨了现代互联网应用中,多级缓存的重要性,特别是Redis在缓存中间件的角色。多级缓存能提升数据访问速度、系统稳定性和可扩展性,减少数据库压力,并允许灵活的缓存策略。浏览器本地内存缓存和磁盘缓存分别优化了短期数据和静态资源的存储,而服务端本地内存缓存和网络内存缓存(如Redis)则提供了高速访问和分布式系统的解决方案。服务器本地磁盘缓存因I/O性能瓶颈和复杂管理而不推荐用于缓存,强调了内存和网络缓存的优越性。
1604 47
|
缓存 前端开发 Linux
哇塞!NPM 缓存竟成开发拦路虎?快来掌握清空秘籍,开启前端开发逆袭之旅!
【8月更文挑战第20天】NPM是前端开发中管理依赖的关键工具。有时需清空其缓存以解决版本不一致或包损坏等问题,确保使用最新依赖。可通过命令`npm cache clean --force`强制清空全部缓存,或手动删除各系统下的缓存文件夹。注意清空缓存可能延长后续安装时间,建议事先备份依赖或确保可重新安装。正确管理缓存有助于提升开发效率。
966 1
|
缓存 JavaScript 前端开发
【性能革命!】Vue 3事件监听缓存的奥秘 —— 揭开前端优化的神秘面纱,让应用性能飙升的秘密武器!
【8月更文挑战第7天】随着前端应用日益复杂,性能优化变得至关重要。Vue 3 通过引入事件监听缓存等新特性提升了应用性能。此特性避免了重复注册相同的事件监听器,减少了资源浪费和潜在的内存泄漏问题。在 Vue 3 中,事件监听器首次渲染时注册,并在后续渲染中重用,除非组件状态变更或手动更新。通过一个示例组件展示了如何利用该特性优化性能,包括使用 `watchEffect` 或 `watch` 在状态变化时重新注册监听器。这一机制降低了浏览器负担,减少了内存占用,提高了应用响应速度,尤其对大型应用效果显著。合理运用事件监听缓存能够构建出更加流畅的应用体验。
877 3
|
前端开发 Java Go
从前端到后端:构建现代化Web应用的技术实践
本文将介绍如何通过前端和后端技术相结合,构建现代化Web应用的技术实践。我们将探讨前端开发、后端架构以及多种编程语言(如Java、Python、C、PHP、Go)在构建高效、可扩展的Web应用中的应用。
|
存储 缓存 前端开发
【Flutter前端技术开发专栏】Flutter中的图片加载与缓存优化
【4月更文挑战第30天】本文探讨了 Flutter 中如何优化图片加载与缓存,以提升移动应用性能。通过使用图片占位符、压缩裁剪、缓存策略(如`cached_network_image`插件)以及异步加载和预加载图片,可以显著加快加载速度。此外,利用`FadeInImage`、`FutureBuilder`和图片库等工具,能进一步改善用户体验。优化图片处理是提升Flutter应用效率的关键,本文为开发者提供了实用指导。
1491 0
【Flutter前端技术开发专栏】Flutter中的图片加载与缓存优化
|
缓存 算法 前端开发
前端 JS 经典:LRU 缓存算法
前端 JS 经典:LRU 缓存算法
427 0
|
存储 缓存 JavaScript
【前端 - Vue】之 Keep-Alive缓存组件使用语法及原理解析,超详细!
【前端 - Vue】之 Keep-Alive缓存组件使用语法及原理解析,超详细!
|
人工智能 前端开发 算法
未来互联网发展趋势分析与前端技术应用探讨
【2月更文挑战第3天】随着人工智能、大数据、物联网等新兴技术的快速发展,未来互联网的发展方向和趋势变得更加多样化和复杂。在这种背景下,前端技术作为用户与系统之间的桥梁,扮演着至关重要的角色。本文将从未来互联网发展趋势的角度出发,探讨前端技术在不断变革的互联网时代的应用与发展。
|
存储 缓存 前端开发
学习和理解前端缓存
前端缓存通过存储重复资源提升网页加载速度,减少服务器压力,优化用户体验。它涉及静态资源(如图片、CSS、JS)的HTTP缓存,动态数据(使用`localStorage`、`IndexedDB`)缓存,用户登录态、页面状态管理,以及预加载缓存。实现方式包括HTTP缓存(强缓存、协商缓存),浏览器存储(localStorage、sessionStorage、IndexedDB),Service Worker和Cache API。在项目中,应根据资源特性和需求选择合适的缓存策略。
|
缓存 负载均衡 前端开发
前端反向代理的神奇世界:加速、安全与缓存的秘密(上)
前端反向代理的神奇世界:加速、安全与缓存的秘密(上)
前端反向代理的神奇世界:加速、安全与缓存的秘密(上)

热门文章

最新文章

  • 1
    前端如何存储数据:Cookie、LocalStorage 与 SessionStorage 全面解析
    1238
  • 2
    【CSS】前端三大件之一,如何学好?从基本用法开始吧!(九):强势分析Animation动画各类参数;从播放时间、播放方式、播放次数、播放方向、播放状态等多个方面,完全了解CSS3 Animation
    553
  • 3
    【CSS】前端三大件之一,如何学好?从基本用法开始吧!(八):学习transition过渡属性;本文学习property模拟、duration过渡时间指定、delay时间延迟 等多个参数
    427
  • 4
    【CSS】前端三大件之一,如何学好?从基本用法开始吧!(七):学习ransform属性;本文学习 rotate旋转、scale缩放、skew扭曲、tanslate移动、matrix矩阵 多个参数
    428
  • 5
    【CSS】前端三大件之一,如何学好?从基本用法开始吧!(六):全方面分析css的Flex布局,从纵、横两个坐标开始进行居中、两端等元素分布模式;刨析元素间隔、排序模式等
    539
  • 6
    【CSS】前端三大件之一,如何学好?从基本用法开始吧!(五):背景属性;float浮动和position定位;详细分析相对、绝对、固定三种定位方式;使用浮动并清除浮动副作用
    716
  • 7
    【CSS】前端三大件之一,如何学好?从基本用法开始吧!(四):元素盒子模型;详细分析边框属性、盒子外边距
    1393
  • 8
    【CSS】前端三大件之一,如何学好?从基本用法开始吧!(三):元素继承关系、层叠样式规则、字体属性、文本属性;针对字体和文本作样式修改
    308
  • 9
    【CSS】前端三大件之一,如何学好?从基本用法开始吧!(二):CSS伪类:UI伪类、结构化伪类;通过伪类获得子元素的第n个元素;创建一个伪元素展示在页面中;获得最后一个元素;处理聚焦元素的样式
    1160
  • 10
    【CSS】前端三大件之一,如何学好?从基本用法开始吧!(一):CSS发展史;CSS样式表的引入;CSS选择器使用,附带案例介绍
    497