热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

webstorm打包rn项目_京东PLUS会员项目前端性能优化实践

京东PLUS会员项目是国内第一个电商付费会员项目,正式开通的会员数量已破千万。我团队从2016年接手这个项目的前端开发工作,一路见证了它的高速成长&#x

京东PLUS会员项目是国内第一个电商付费会员项目,正式开通的会员数量已破千万。我团队从2016年接手这个项目的前端开发工作,一路见证了它的高速成长,也为此贡献了自己的力量。

这个项目有几个特点:

第一,需求多。移动端使用 H5 开发,曾有人问为什么不用原生或者 RN 开发? 我觉得吧,以这个项目的需求数量和迭代速度来看,连 H5 都难以 hold 的住,还是不要奢望原生和 RN 了。

第二,产品经理多。一般的项目对接一两个产品经理,这个项目我们需要对接一个异地的产品经理“团队”;一般的项目换产品经理一个一个的换,这个项目一批一批的换……我们已经送走好几届PLUS会员产品经理了。铁打的研发,流水的产品经理。

01a2add7fc5696cf496e20407372d16d.png

所以说,PLUS会员项目是业务方滴,也是项目经理滴,还是产品经理滴,但终归是俺们研发滴。每念及此,我的耳边总会响起叶倩文的那首老歌:“天地悠悠,过客匆匆,潮起又潮落...”。

书归正传。用户众多和需求迭代频繁,确保线上安全稳定始终是第一要务。所以在架构调整和性能优化方面我们一直都小心翼翼,以一些小修小补为主,只有到大规模改版的时候才会有大的升级改造。不过,平时我们对这些问题的思考和实践却不曾停止过,我们验证了一些行之有效的优化方案,在下一波改版中将会得到应用。

43afcd6d22258a91a6f4174f5b31f358.png

我虽然不完全认同“前端开发每十八个月难度翻一倍”的说法,但这一行发展迭代速度快却是不争的事实。若等到这些优化方案全都应用上再出来念叨,可能就显得不那么新鲜了。所以,我决定先把这些方案拿出来分享,和感兴趣的小伙伴一起讨论,进一步完善。

这些方案主要针对移动端,优化核心方向是提高首页的加载速度,特别是首屏和弱网络环境下的加载速度。从持久化缓存、削减代码量、优化接口请求、提升主观感受等方面下手,比较大的改动是应用 PWA 和升级架构。 PWA 离线缓存可以极大的提升用户体验,不过它对于首次加载速度并无提升作用,还得靠其他优化手段,这是一套组合拳。我们先从架构升级说起吧。

一、架构升级

项目计划迁移到 Gaea4.0 脚手架[1],这是我们团队基于 webpack 4 开发的一套通用 Vue 单页面应用脚手架,此前的系列版本已经过数十个项目的验证,还是比较稳定的。近期新推出4.0版相较之前版本有着不小的改进。

  • webpack 升级到了 4.0

  • Babel 升级到了 7.0

  • Vue-loader 升级到了 15

  • 重构了上传插件,一键上传到测试服务器更快更稳定

  • 针对我厂手机和电脑位于不同局域网无法互访的问题,集成了自主研发的 Carefree 解决方案[2],方便真机测试调试

  • 集成了 NutUI 组件库[3],可按需加载需要的UI组件

  • 集成了自主研发的基于swagger的数据mock工具SMOCK[4]

  • 支持自动生成骨架屏[5]

  • 支持 PWA

迁移有几个主要目的:

首先,实现本项目的 webpack 构建工具升级到 4.0,之前是基于 webpack 2.0 开发的,webpack4 有不少提升,比如:

  • Scope Hoisting(作用域提升,webpack3加入),通过减少闭包函数数量加快JS的执行速度

  • 生产环境构建体积更小

  • 开发环境通过优化的增量构建机制提升构建速度,同时提供详细的错误和提示

其次, Gaea4.0 的 Babel 是 7.0 版的,基于 Babel7 可以实现更智能的 Babel polyfill 按需加载。

再次,本次优化计划尝试的PWA、骨架屏等方案, Gaea4.0都可以给予基础支持。

最后, Gaea4.0 集成的Carefree、新的上传插件等功能将给未来的开发和真机调试带来方便。

二、Babel polyfill的按需加载

如今的 web 应用开发都是在本地进行构建,所以有条件在构建阶段把高版本的 JS 代码编译成低版本语法,这样既使用了新语法,又解决了低版本浏览器的兼容问题。承担这种转换工作的最知名的工具当属 Babel 了。而一直以来,Babel 有个饱受诟病的地方,那就是 polyfill 问题。

Babel 默认只转换 Javascript 语法,而不转换新的 API,比如 Promise、Generator、Set、Maps、Symbol 等全局对象,一些定义在全局对象上的方法(比如 Object.assign)也不会被转码。如果想让未转码的 API 可在低版本环境正常运行,这就需要使用 polyfill。

polyfill 有多种方案,各有各的问题。目前应用中通常使用 babel-polyfill 方案,而第三方库中通常使用 babel-runtime 和 babel-plugin-transform-runtime 方案。

babel-polyfill 提供完整的环境垫片,包含所有 API 的降级模块,可以为新的 API 和全局对象上的方法提供兜底,其主要缺点是文件较大,压缩后大概八九十KB。目前项目中采用这种方案,这次考虑予以优化,减少加载的代码体积。

如上文提到,这一波改造会把项目迁移到 Gaea4.0 脚手架中,新脚手架的 Babel 已经升级到了最新的 7.0 版。Babel7 是 Babel6 推出近三年之后发布的一个断崖式升级的大版本,包含很多新特性,其中一个引人关注的特性就是支持更智能的按需加载 polyfill。

Babel7 主要是通过其提供的 @babel/preset-env 实现按需加载的。

使用 @babel/preset-env 也需要首先安装 @babel/polyfill,但最终打出的包并不会导入全部 polyfill。

npm install @babel/polyfill --save

同时,需要在 .browserslistrc 文件或者 .babelrc 的 targets 字段中指定需要兼容的浏览器范围。

之后在.babelrc文件中对 @babel/preset-env 进行配置。

@babel/preset-env 与按需加载 polyfill 相关的选项是 useBuiltIns,它有两个值需要重点关注: entryusage

当值为 entry 时,Babel 会将 import"@babel/polyfill" 或者 require("@babel/polyfill") 语句根据我们指定的环境配置替换为单个的 polyfill require。

如将

import "@babel/polyfill";

替换为

import "core-js/modules/es7.string.pad-start";

import "core-js/modules/es7.string.pad-end";

当值为 usage 时,更加智能。Babel 会根据每个文件的需要和指定的环境配置添加特定的 polyfill,更牛×的是一个 bundle 中相同的 polyfill 只会加载一次,这也有助于减小 bundle 的体积。推测 Babel 是通过对文件进行静态分析实现的这种精准的按需加载 polyfill 功能。

var a = new Promise();

转换后(如果指定的环境不支持)

import "core-js/modules/es6.promise";

var a = new Promise();

转换后(如果指定的环境支持)

var a = new Promise();

我们尝试了一下,先指定需要兼容的浏览器范围,然后安装 @babel/polyfill 并将 @babel/preset-envuseBuiltIns 选项值设为 usage。这样 Babel 就会自动分析每一个文件并在考虑我们指定的浏览器兼容范围的情况下,为每个文件加载其需要的 polyfill。最终项目里只引入了部分 polyfill,经测算,打包后的代码(min)较直接引入完整 babel-polyfill 的方案小60多KB,同时还避免了全局变量污染。

在 Babel 的配置中开启 Debug 模式,构建的时候可以看到每个文件中添加了哪些 polyfill:

85328fcc457df6a32c7fd09800c5bd85.png(有从知乎远道而来的杠精问到:“这都什么年代了,还在兼容Android 4.0和iOS 8.0?”我叹口气、耸耸肩,与该杠精握握手…)

关于这个问题的进一步思考:

这种加载 polyfill 的方式已经比传统方式先进了很多,但还是不完美,比如按照我们指定的浏览器范围需要引入的某个 polyfill,对于高版本浏览器来说可能还是多余。

个人觉得一种比较理想的方案是先在编译阶段通过静态分析确定可能需要 polyfill 的 API 范围但并不打包 polyfill 进去,而是当用户在浏览器中访问这个页面时,通过植入页面的JS脚本逐一检测当前浏览器是否支持这些新的 API,把不支持的找出来,通过一个请求去服务端加载对应的 polyfill 文件。当然这需要类似 polyfill.io 的服务端 polyfill 方案支持。未来我们会沿着这个方向继续探索。

三、持久化缓存

PWA 是真的火了,现在的项目里没用 PWA 出门都不好意思跟人打招呼。 PWA 的一系列功能中最重磅的非离线缓存莫属了,虽说 H5 之前就有离线缓存(application cache)API,可惜不好用, PWA 离线缓存足以把它拍死在沙滩上。

从业务角度来讲,我们认为本项目不太适合离线访问,但我们可以利用 PWA 把静态资源进行离线缓存,提高页面访问速度。

在这种场景下,用 ServiceWorker 不缓存页面自身 HTML 和接口数据,只缓存静态资源,且优先使用缓存。非首次访问的情况下,静态资源都会走缓存,页面访问速度得以大幅提升。

但有一个问题,就是页面更新的问题。使用缓存优先策略,意味着每次进入页面时,在有缓存的情况下直接使用缓存。如果缓存有更新,在缓存更新之后需要刷新页面才能看到变化。自动刷新页面严重影响用户体验,而提示用户去手动刷新,在 APP 里看上去也有些奇怪,且不是所有有用户都会去手动刷新的。对于PLUS会员这种需求排队,更新频繁的项目,用户感受到的影响可能会更多。HTML5 的离线缓存 API 也有这个问题,这当然不是一个缺陷,而是“优先使用缓存”策略所决定的,只是不完全满足我们的需求罢了。

针对这个问题,我们的解决方案是当文件有更新时,同时修改缓存的版本号和页面中引用这个文件的 URL 中的版本号,让浏览器直接使用新文件,不使用缓存。在页面加载之后,缓存也会更新,下次访问时,还会走缓存。

这个方案还有优化空间,只有那些有变化的文件需要更改 URL 中的版本号,使用新文件,而页面中其他没有发生变化的静态资源还是可以也应该继续使用缓存。按照这个思路,我们应把代码中稳定的、不常变化的模块(比如 Vue 及其插件)尽量提取出来,让这部分内容尽可能使用缓存,当然必要的时候也可以通过相同的方式更新。而经常发生变化的部分(如业务代码)应独立打包,体积越小越好,以减小页面和缓存更新时的开销。

对于这些稳定公共模块的提取我们使用 webpack 内置的 DllPluginDllReferencePlugin 插件来实现,通过这两个插件提前对这些公共模块进行独立编译,打出一个 vendor.dll.js 的包,之后在这部分代码没有改动的情况下不再对它们进行编译,所以项目平时的构建速度也会提升不少。vendor.dll.js 包独立存在,hash 不会发生变化,特别适合持久化缓存。

于是,我们的业务代码有变化时,只需要以新版号发布业务包(app.js)即可,vendor.dll.js 依然使用本地缓存。

我们来看一下具体的加载情况。

首次访问,没有 PWA 缓存,所有资源都走线上。页面加载之后,PWA会缓存静态资源。

79c2bcc2fdaf0059e79932d4e878b406.png

之后的访问,静态资源优先从缓存加载,速度极快。

6605281b199c257a0bcbbbd93cedd83b.png

当业务代码有更新时,更改页面中引用 app.js 文件的 URL 中的版本号,使得 app.js 不使用缓存,已缓存的其他静态资源依然可以使用缓存。同时更改缓存的版本号,缓存也会在页面加载之后更新,新的 app.js 文件也会被缓存。

bc9af05d4687d93025e8eda291e33e02.png

再次访问时,包括 app.js 在内的静态资源依然全部走缓存。

0d1a415967a99c2f5621995596254126.png

四、请求优化

这个是一个前后端分离的项目,前端是标准的 Vue SPA,完全通过接口同后端进行数据交互。PLUS会员业务逻辑本身比较复杂,涉及很多种用户状态,页面逻辑也复杂。不同用户看到的界面不完全相同,这受用户状态和后台配置等多种因素影响。

部分接口存在相互依赖的关系,比如有接口要求传用户状态,因此需要先行通过用户信息接口拿到用户状态。再比如商品数据接口,需要先请求楼层配置信息接口,确定当前页面有哪些楼层,继而才能决定去请求哪些楼层的数据。

这种串行的接口请求拖慢了首屏的渲染,这是目前影响首页性能的一个主要问题,也是这次优化的一个重点。

服务端渲染(如Vue SSR),首屏直出当然是最理想的方案。但目前看来并不现实,这个项目的研发团队情况也比较复杂,前后端是两个跨职场、跨部门的团队,且需求巨多,页面改动频繁。完全的前后端分离更有助于明确职责,提高效率,减少扯皮。

另一个折中的方案是,在页面上直接引一个后端的模板文件,后端研发同事通过这个模板文件把用户状态、楼层配置等前置信息打到页面上,页面在浏览器中初始化的时候直接读取这些信息,然后再去请求那些依赖这些数据的接口。这样即可避免串行请求的问题,同时还减少了几个请求,有助于提高页面加载和渲染速度。这次优化,我们计划采用这种方案。

优化前:

87b24aad4946f45e197f981ca318f09d.png

优化后,关键请求大幅提前:

9fe143f2b60963193f49ded199a3c250.png

优化前:

1a3d6f75d1ee4a7bbad74ef69caca902.png

优化后,页面开始渲染的时间明显提前:

774c93a424c402c45c116298a9d34f38.png

梦想还是要有的。前后端分离是一种进步,但彻底的分离,也不尽善尽美,比如会有首屏加载速度和 SEO 方面的困扰。 前后端分离+服务端首屏渲染 看起来是个更优的方案,它结合了前后端分离和服务端渲染两者的优点,既做到了前后端分离,又能保证首页渲染速度,还有利于 SEO。但在 Vue、React 等前端框架大行其道的今天,服务端渲染早已不是当年套 HTML 页面那么简单了,即便只渲染个首屏。前后端同构可能是比较好的解决方案,而这种场景下服务端渲染工作显然由前端来承担更合适,所以用 Node.js 搞个中间层是必要的。

b68d65150192db00431661c6e5a2996e.png

五、骨架屏

通过一系列优化,除了客观上首屏渲染时间的明显缩短,我们还额外给页面加上了骨架屏(skeleton screen),让用户主观感受到的页面加载和渲染速度比真实情况还快。虚虚实实,用兵之道也,一切为了用户体验。

先来了解一下骨架屏的概念。骨架屏指的是在页面数据加载完成前,先给用户展示出的页面大致结构,之后渲染出真实页面内容将其换掉。这是近两年流行起来的加载控件,本质上是界面加载过程中的过渡效果。

在加载完成前把网页的大概轮廓预先显示,接着逐渐加载真正内容,这样既可缓解用户等待的焦灼情绪,又能使界面的加载过程显得更自然通畅,减少了长时间白屏或者闪烁。骨架屏能给人一种页面内容“已经渲染出一部分”的感觉,相较于传统的 loading 效果,体验更佳。

我们团队对骨架屏技术有比较深入的研究,开发过一个名为 @nutui/draw-page-structure [4]的webpack插件,可实现通过 puppeteer 自动生成纯 DOM 形式的页面骨架屏,并支持自动插入到指定页面。如果对自动生成的效果不满意,还允许定制和调整。

我们用这个插件在项目里小试了一把,效果还是不错滴。纯 DOM 形式的骨架屏代码,比图片、Canvas等形式数据量更小,调整起来也更灵活。

a180354feb787ab27050c3da8aa0b7fe.png

六、图片格式

Plus会员频道首页是一个典型的电商页面,包含大量的图片。使用新兴的图片格式可以大大减少加载的图片体积,并有助于提升图片的解析和渲染速度,进而提升页面渲染速度。对于移动web来说,还有一个重要的优点——节省用户的流量(中国移动30M5块钱呢,哈哈)。

去年我们在项目里应用了 WebP 格式,收效不错。比如某张背景图片,压缩后的 png 格式是35KB,而转成 WebP 只有4KB,两者基本看不出质量上的差别。

新兴图片格式的应用的主要障碍还是兼容性,以 WebP 为例,谷歌系的浏览器以及欧朋浏览器支持情况良好,Firefox、Edge 也都在新版本提供了支持,可惜苹果公司一直没有跟进,Safari 直到现在也没有要支持的迹象,iOS 上的应用如果想支持,还需自行打包解析库(经测试发现iOS版的京东APP已经提供了支持,点个赞)。

我们使用 WebP 的方式是在页面上通过JS判断当前浏览器是否支持 WebP,如果支持,则在 body 上增加一个名为 “webp” 的 class,同时把判断结果写入 localStorage,之后再进入页面时直接从 localStorage 里读取,不用每次都执行判断的代码了。然后在页面的 css 中通过 “.webp” 选择器、在 Vue 的图片过滤器中通过判断结果来决定是否加载 WebP 格式图片。

document.createElement('canvas').toDataURL('image/webp').indexOf('data:image/webp') === 0

这次的优化,我们考虑增加对我厂 DPG 图片格式的支持。

DPG 是我厂基础架构部-智能存储部推出图片压缩技术,经过 DPG 压缩后的图片兼容 jpeg,同时全平台、全部浏览器都支持,DPG 是一种有损压缩技术,但通过5名用户10000张图片的人眼浏览测试,和 WebP 的清晰度对比没有差距。该技术可以有效地减少图片大小50%,减少 CDN 带宽流量 50%,加快图片用户在设备上的渲染速度。

基于我个人的理解, DPG 格式应该是对 jpeg 格式图片通过一定算法进行了二次压缩,其本质上还是 jpeg(虽然扩展名改了),这也才能有所谓”全平台浏览器支持“的可能性。所以,特别适合将 jpeg 格式的图片替换为 DPG 格式,当然前提是服务器上有 DPG 格式图片。我厂的图片系统会自动生成上传图片对应的 DPG 格式图片。所以我们定的 DPG 格式使用条件就是原图是 jpeg 格式,且图片位于我厂图片系统中。在兼顾既有的 WebP 格式图片加载逻辑的基础上,我们梳理后的图片加载逻辑如下图所示:

28bf7673d59824fad1b105181c97360a.png

先聊到这里吧,我去参加PLUS会员项目的需求评审了……

七、扩展阅读

[1] https://www.npmjs.com/package/gaea-cli

[2] http://carefree.jd.com

[3] http://nutui.jd.com

[4] http://smock.jd.com

[5] https://www.npmjs.com/package/@nutui/draw-page-structure

6b32a6c44b169fa9aeea4718769f062d.png




推荐阅读
  • 本文介绍了OkHttp3的基本使用和特性,包括支持HTTP/2、连接池、GZIP压缩、缓存等功能。同时还提到了OkHttp3的适用平台和源码阅读计划。文章还介绍了OkHttp3的请求/响应API的设计和使用方式,包括阻塞式的同步请求和带回调的异步请求。 ... [详细]
  • 本文介绍了Python高级网络编程及TCP/IP协议簇的OSI七层模型。首先简单介绍了七层模型的各层及其封装解封装过程。然后讨论了程序开发中涉及到的网络通信内容,主要包括TCP协议、UDP协议和IPV4协议。最后还介绍了socket编程、聊天socket实现、远程执行命令、上传文件、socketserver及其源码分析等相关内容。 ... [详细]
  • 生成式对抗网络模型综述摘要生成式对抗网络模型(GAN)是基于深度学习的一种强大的生成模型,可以应用于计算机视觉、自然语言处理、半监督学习等重要领域。生成式对抗网络 ... [详细]
  • SpringBoot uri统一权限管理的实现方法及步骤详解
    本文详细介绍了SpringBoot中实现uri统一权限管理的方法,包括表结构定义、自动统计URI并自动删除脏数据、程序启动加载等步骤。通过该方法可以提高系统的安全性,实现对系统任意接口的权限拦截验证。 ... [详细]
  • Android中高级面试必知必会,积累总结
    本文介绍了Android中高级面试的必知必会内容,并总结了相关经验。文章指出,如今的Android市场对开发人员的要求更高,需要更专业的人才。同时,文章还给出了针对Android岗位的职责和要求,并提供了简历突出的建议。 ... [详细]
  • Voicewo在线语音识别转换jQuery插件的特点和示例
    本文介绍了一款名为Voicewo的在线语音识别转换jQuery插件,该插件具有快速、架构、风格、扩展和兼容等特点,适合在互联网应用中使用。同时还提供了一个快速示例供开发人员参考。 ... [详细]
  • XML介绍与使用的概述及标签规则
    本文介绍了XML的基本概念和用途,包括XML的可扩展性和标签的自定义特性。同时还详细解释了XML标签的规则,包括标签的尖括号和合法标识符的组成,标签必须成对出现的原则以及特殊标签的使用方法。通过本文的阅读,读者可以对XML的基本知识有一个全面的了解。 ... [详细]
  • 本文介绍了Web学习历程记录中关于Tomcat的基本概念和配置。首先解释了Web静态Web资源和动态Web资源的概念,以及C/S架构和B/S架构的区别。然后介绍了常见的Web服务器,包括Weblogic、WebSphere和Tomcat。接着详细讲解了Tomcat的虚拟主机、web应用和虚拟路径映射的概念和配置过程。最后简要介绍了http协议的作用。本文内容详实,适合初学者了解Tomcat的基础知识。 ... [详细]
  • flowable工作流 流程变量_信也科技工作流平台的技术实践
    1背景随着公司业务发展及内部业务流程诉求的增长,目前信息化系统不能够很好满足期望,主要体现如下:目前OA流程引擎无法满足企业特定业务流程需求,且移动端体 ... [详细]
  • CentOS 7部署KVM虚拟化环境之一架构介绍
    本文介绍了CentOS 7部署KVM虚拟化环境的架构,详细解释了虚拟化技术的概念和原理,包括全虚拟化和半虚拟化。同时介绍了虚拟机的概念和虚拟化软件的作用。 ... [详细]
  • 浏览器中的异常检测算法及其在深度学习中的应用
    本文介绍了在浏览器中进行异常检测的算法,包括统计学方法和机器学习方法,并探讨了异常检测在深度学习中的应用。异常检测在金融领域的信用卡欺诈、企业安全领域的非法入侵、IT运维中的设备维护时间点预测等方面具有广泛的应用。通过使用TensorFlow.js进行异常检测,可以实现对单变量和多变量异常的检测。统计学方法通过估计数据的分布概率来计算数据点的异常概率,而机器学习方法则通过训练数据来建立异常检测模型。 ... [详细]
  • Redis底层数据结构之压缩列表的介绍及实现原理
    本文介绍了Redis底层数据结构之压缩列表的概念、实现原理以及使用场景。压缩列表是Redis为了节约内存而开发的一种顺序数据结构,由特殊编码的连续内存块组成。文章详细解释了压缩列表的构成和各个属性的含义,以及如何通过指针来计算表尾节点的地址。压缩列表适用于列表键和哈希键中只包含少量小整数值和短字符串的情况。通过使用压缩列表,可以有效减少内存占用,提升Redis的性能。 ... [详细]
  • 一句话解决高并发的核心原则
    本文介绍了解决高并发的核心原则,即将用户访问请求尽量往前推,避免访问CDN、静态服务器、动态服务器、数据库和存储,从而实现高性能、高并发、高可扩展的网站架构。同时提到了Google的成功案例,以及适用于千万级别PV站和亿级PV网站的架构层次。 ... [详细]
  • 本文介绍了Android中的assets目录和raw目录的共同点和区别,包括获取资源的方法、目录结构的限制以及列出资源的能力。同时,还解释了raw目录中资源文件生成的ID,并说明了这些目录的使用方法。 ... [详细]
  • 本文介绍了H5游戏性能优化和调试技巧,包括从问题表象出发进行优化、排除外部问题导致的卡顿、帧率设定、减少drawcall的方法、UI优化和图集渲染等八个理念。对于游戏程序员来说,解决游戏性能问题是一个关键的任务,本文提供了一些有用的参考价值。摘要长度为183字。 ... [详细]
author-avatar
Mr_cool
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有