[性能优化实践] 单页应用

2,747 阅读7分钟
原文链接: smallpath.me

SPA(单页应用)的性能优化策略, 与传统MVC框架直出的策略有很多不同之处.
本文介绍博客前端的SPA优化策略, 以及一些常见的通用优化办法

SPA策略

XHR过滤不需要的属性

以本博客首页为例, 请求的post模型中有summary, content, markdownContent三个较大的属性, 对首页而言contentmarkdownContent并没有用

本博客依此做了一个过滤, 未优化前一次请求为160ms, 优化后一次请求仅为40ms. 不过,SSR首屏后, 这种优化带来的加速不大.

单向数据流

为了做到每个请求都物尽其用, 单向数据流是最好的选择

假如有两个不同的组件, 同时依赖一个数据, 不用单向数据流的话, 就得发出两个相同的请求.
我认为在这种情况下, 不能用LocalStorage来解决, 毕竟两个组件哪一个首先挂载好这件事是没有定论的

本博客前台未使用单向数据流前, 向/api/option发出了两次相同请求, 每个请求50ms, 优化后获得了50ms的提升

服务端渲染(SSR)

SSR是SPA的终极目的地了, 除了可以解决SEO外, 还可以大幅度降低首屏加载速度.
至于为什么不说非首屏的加载速度, 要知道SPA非首屏的页面, 大概率只需要发一个XHR而已, 因此只需要解决首屏渲染的问题

SSR依靠的服务端可以是express或者koa2, 这个服务端推荐与数据库放在内网甚至是同一个服务器上, 这样通过内网访问, 可以大幅度缩短XHR请求的耗时.

展示类型的单页应用的SSR中,获取到的数据中,_id属性用处也不大,同样可以过滤掉,能省1k是1k

本博客通过SSR, 将首屏访问时间从平均800ms降低至平均300ms, 其他简单页面比如关于页面, 甚至降低到了200ms以内

分块懒加载

分块异步加载也是SPA的一种常见首屏优化策略, 指将首屏中不需要的组件分割开, 在访问其他路径时才加载服务器的另一个块来获取需要的组件

不过它与SSR有一些冲突的地方, 例如目前vue2的SSR与分块懒加载是不能共存的, 不过好消息是vue2的一名核心开发者正在解决这个问题

SSR组件级缓存

通过id+更新时间缓存了列表中的每一条数据, 因此给博客首屏带来了50ms的优化

减少首页数据

以屈屈为例, 他的首屏仅仅几百字, 而我的首屏之前总共有几千字, 特别是SSR需要将vuex初始状态也序列化到页面中, 因此我首屏里的文字大小实际上是屈屈的20倍左右

尽量减少DOM后, 页面大小减少了30kb, gzip从96kb降到了90kb

合并js文件

这个策略是针对未开启http2多路复用的SPA的. 特别是打包时一定要合并vendor

开了多路复用后, 打包反而会增加耗时, 毕竟复用的那一个TCP链接能省不少时间

mongoDB分页优化

mongoDB的skip是非常慢的.

有一种优化策略如下:

假设每页10条, 那么请求第一页的时候, 额外多请求1条, 即请求11条. 如果返回11条, 则说明有下一页, 如果不足11条, 则说明无下一页 接下来, 请求第二页的时候, 将第一页的第10条的id拿到作为基准id, 通过find({"_id":{"$gt": 'the id'}}).limit(11)查询下一页的11条.

这样有一个弊端, 即无法直接访问非第一页, 因为没有基准id.
不过, 像博客这种小流量的系统, 我们可以将第1, 第11, 第21等等的_id通过redis缓存起来,在客户端请求分页时直接用redis中的_id去查询接下来的10条即可

避免使用大型类库

都用上SPA了, 还是对大型类库说拜拜吧.
react和vue都支持通过挂载ref直接获取dom, 这时候就该原生JS出马了.
我觉得这一点应该是前端的进步, 无论前端怎么变, 原生JS永远是大厦的根基

tree-shaking打包减少无用代码

webpack2默认支持这玩意, 修改babelrc后, 前台打包从142k降到了122k 虽然我觉得一脸懵逼: node_modules里没有es6 Module的库, 它是怎么压缩到的??

不过, tree-shaking与分vendor打包有冲突, 因为分了vendor的话, 所有vendor的库都不会被tree-shaking到, 以本博客前台为例, tree-shaking且不分vendor时打包为一个122k的app块, tree-shaking且分vendor时打包22k的app块与120k的vendor块.

这时如果开启了http2多路复用的话, 真的不好说是分vendor复用的tcp链接节省时间, 还是tree-shaking省下的20k代码更节省时间. 但是在不考虑http2的情况下, tree-shaking毫无争议地比分vendor要好

通用策略

图片优化策略

  1. favicon没有必要大到128x128, 只需要32x32就足够了, 这里可以省下15kb左右大小
  2. SPA应用的静态图片资源一般都放在cdn上, 可以在上传时通过cdn提供的多媒体处理功能, 来生成缩略图, 这样除了加速访问外, 还可以打水印防盗
  3. 经常使用的图片可以换为webp格式, 以博客的logo为例, jpg格式时为20kb, webp格式仅为3kb.

但是, webp格式比较新, 有些旧浏览器并不支持(说的就是我还在用的火狐v41.0), 可以用如下代码将不支持的webp换为其他格式

img.onerror = ({ target }) => (img.src = target.currentSrc.replace('.webp', '.png'))

注意上面的代码是babel转码前的, 要直接用你得转成es5形式

LocalStorage数据缓存

LocalStorage我不大敢用它, 没有校验策略的话它简直是个定时炸弹呀, 而且SPA单向数据化之后, 需要的数据都可以从全局状态树中获得, 没有必要上LocalStorage

不过用好了还是非常强大的, 例如屈屈大神的博客, 首屏加载150ms, 我也是醉了, 硬是将一个MVC框架直出的博客弄成了单页应用, 还保证了首屏没有无用的全局状态数据(通常20kb)

使用Gzip压缩

Gzip通常能够压缩70%的文件大小, 而且对nginx来说配置也很简单, 默认的几行就开启好了. 对于单页应用来说, 请不要忘记让rest api也被gzip, 以博客一个返回3k大小的xhr为例, gzip后能省2ms, 比起压response的header得到同样2ms的加速, 总要轻松得多吧?

DOM加载优化

屈屈大神的博客这一点很明显, 首页的摘要不是Markdown解析出来的html标签, 而是纯粹的文本, 可以说是优化到了细节处

nginx升级到最新版

比较老的linux发行版自带的nginx, 版本一般都比较低, 例如本博客之前就是1.4.4, 从1.4.4升级到1.11.5后, 即使未开启https和h2, 访问速度都稳定并提高了一大截

开启HSTS

HSTS可以避免http到https跳转, 除了安全之外, 也能够省去一次http完整请求的时间, 对首屏访问也非常重要.

别忘了最后去申请HSTS preload list

启用HTTP2多路复用

图片懒加载

这个也是常见的通用策略, 略过

效果

之前, vue2单页应用, 并且没开SSR, 首屏强刷976ms: before

之后, 按本文的顺序优化后, 并且开启SSR, 首屏强刷163ms: after

实际上163ms的图片是在优化完成前截的. 现在换了延迟极低的hk服务器后, 博主这边甚至能压到150ms以内

本文链接:smallpath.me/post/spa-op…