端到端拉通实现Web首屏优化
2018-06-13 / modified at 2022-04-04 / 2.4k words / 9 mins
️This article has been over 2 years since the last update.

随着业务的增加,客户对IT系统的前端速度不太满意,希望通过改造提高速度。

本文场景是内网企业级应用(SQL多,表格多,图片少)的首屏速度加载优化

1. Backgroud

随着项目业务越做越大,前端也越来越臃肿。经过分析,有如下质量问题

  • 项目本身有历史债务,由于人员变化导致很多没用到的js不敢删了
  • 由于项目是自有人员带外包的模式,导致js代码质量难以控制
  • 研发人员以Java后端为主,本次优化不希望提高研发的学习成本

2. Requirement

目前项目首页速度已经高达5s,希望通过前后端拉通将速度降低到2.5s(含各种复杂的查询)

3. Action

众所周知,让界面加快无非几种方法

  • 压缩js/css为单个文件
  • 开启gzip/缓存
  • 使用asm.js /Worker 线程
  • 通过组件化分批按需加载js/html(比如Angular中的component实现企业级SPA应用)
  • 后端接口的SQL优化/Redis缓存/Sleuth记录等

3.1. 压缩JS/CSS

虽然网上有很多基于NodeJS的压缩项目,但是我个人更偏好在Java端使用纯maven实现。最终选择了基于谷歌ClosureCompiler的第三方minify-maven-plugin项目

从Demo开始学习

开源项目的特点就是文档不是很全,需要自己去找Demo或者看源码,毕竟纯情怀做高质量开源是一件吃力的事情,因此

导入与使用Demo方法

1
2
3
4
5
git clone https://github.com/samaxes/minify-maven-plugin.git              
git checkout minify-maven-plugin-1.7.6
mvn clean install
cd demo
mvn clean package

如何给maven plugin 打断点

打开IDEA导入刚刚项目,注意demo项目也要导入。

进入MinifyMojo.java文件,对感兴趣的位置打上断点

然后点击maven project ,选择Minify Maven Plugin Demo,在生命周期中的package右键选中debug,这时就可以发现断点进入了,读者就可以自行分析代码了。

这个也是快速阅读开源项目的一个技巧,通过断点降低陷入文档的时间

插件配置

maven配置中的所有配置可以参考MinifyMojo这个对象,下面是我的定制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<profiles>
<profile>
<id>minify</id>
<build>
<plugins>
<plugin>
<groupId>com.samaxes.maven</groupId>
<artifactId>minify-maven-plugin</artifactId>
<version>${project.version}</version>
<executions>
<execution>
<id>bundle-configuration-minify</id>
<configuration>
<bundleConfiguration>src/minify/static-bundles.json</bundleConfiguration>
<jsEngine>CLOSURE</jsEngine>
<jsSourceDir><![CDATA[/]]></jsSourceDir>
<closureCreateSourceMap>true</closureCreateSourceMap>
</configuration>
<goals>
<goal>minify</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>

外部json(src/minify/static-bundles.json)配置,注意这个要注意文件的前后依赖性(比如JQuery插件不能优先加载),因此不要使用通配符偷懒

1
2
3
4
5
6
7
8
9
10
11
12
{
"bundles": [
{
"type": "js",
"name": "js/all/static-combined.js",
"files": [
"js/samaxesjs.core.js",
"js/subdir/samaxesjs.toc.js"
]
}
]
}

如果你希望手动打包的话,那么可以在Terminal中运行运行

1
2
# -P minify表示激活了minify的配置,如果你不要压缩,那么可以去掉它以减少打包时间
mvn com.samaxes.maven:minify-maven-plugin:1.7.6:minify -P minify

如果你在IDEA下希望失去焦点后能够自动刷新打包,可以在Maven Projects选项中选择

1
Plugins -> minify -> minify:minify(Right Click) -> Execute Before build

这样你就可以聚焦业务,不用折腾打包流程了。

3.2. 降低传输数据量

请求侧:降低Header信息量

降低Cookies的payload

由于很多企业级项目的Cookies非常多,而这些Cookies只要是在同一个域名下,浏览器将始终拼砖发送,因此冗余的Cookies可以考虑干掉,或者切换到x-auth-token这种更安全的实现。

为静态文件使用不同的域名

当普通的图片与业务均共享使用同一个域名时,浏览器请求图片等资源时也会带上Cookies,Token等Header信息,导致请求Payload耗时变高。此问题除了显性配置set-cookies,还有使用CDN域名规避的方法,举个StackOverflow的例子

1
2
API业务: stackoverflow.com
静态资源: cdn.sstatic.net

返回侧:开启Gzip并配置缓存

此处有两种方法,一种是配置在Nginx中存放静态文件,还有一种是直接配置tomcat

Nginx配置方法

这个网上很多了,详见serving-static-content。你可以通过查看js返回的Header中是否有Server: Nginx来判断是否生效,一般来说压缩级别配置到5就差不多可以了

Tomcat配置方法

同样网上很多,可以参考这里

1
2
3
4
5
<Connector port="8575" connectionTimeout="20000" redirectPort="8443"
maxThreads="200" minSpareThreads="10" compression="on"
compressableMimeType="text/html,text/xml,text/plain,text/css,application/json,application/javascript,image/gif,image/jpg,image/png"
protocol="org.apache.coyote.http11.Http11NioProtocol"
enableLookups="false" acceptCount="10" debug="0" URIEncoding="UTF-8" />

上面两个方法二选一即可,通过开启Gzip可以将文件压缩到1/3甚至1/4

3.3. 按需加载

按需加载就是客户点进业务详情页面时再去加载html/js,一般常见的单页应用(Single Page Application)均支持按需加载。在本项目中选用了成熟的AngularJS + UI-Router的方案,可以参考这里。下面讲两个常见问题

项目白屏时间变长(400ms)

由于浏览器的主线程是单线程,因此只能使用异步API交给Worker线程池来实现多线程加载。但是有些位置是很难优化的,比如UI-Router是依赖于AngularJS,那么必须在AngularJS初始化完成后才能加载

1
ParseDOM -> AngularJS.inject() -> AngularJS.eval() -> AngularJS.apply() -> UI-Router

由于其他JS与AngularJS本身需要初始化,因此这里的UI-Router以及需要加载的界面只有等待AngularJS初始化完成才能进行请求,可以使用Chrome的Performance工具进行分析调优。

对策如下

  • 升级支持预编译生成静态网站的框架,比如Angular/React,前提是项目成员学习跟得上
  • 降低AngularJS双向绑定的对象,移除主页面的冗余依赖注入对象
  • 使用假CSS实现视觉效果,避免一直白屏

如何压缩按需加载的静态文件

此需求在网上尚未找到一个全自动的方案,因为JS里的URL是需要替换的,目前正在研究中,目前思路是基于分析$State的AST对象

  • 分析AST对象自动生成minify需要的json文件,手动维护一下
  • 分析AST对象将css/js内联到html中

3.4. 后端优化

后端优化是一个很宽泛的话题,以Tomcat为例,常见的调用栈如下

1
Tomcat -> filterChain -> SpringMVC -> Inteceptors -> Controller -> Service(RPC/SQL)

常见瓶颈如下

过滤器(FilterChain, Inteceptors, AOP)

此处包括Tomcat中的过滤器,SpringMVC中的拦截器,以及Controller中暗藏的AOP

  • 假如在Session过滤器中拦截了js的请求,很可能导致下载一个简单的js都需要去请求Redis => 过滤器不要偷懒使用/进行全局过滤,而只用把RESTful请求过滤即可
  • 在访问记录等过滤器中使用了插入SQL的方式记录导致阻塞 => 应该使用MQ替换掉阻塞任务
  • 至于AOP,我个人认为是一种反模式,如果缺乏自动提示的IDE,还真很难定位出AOP的位置

业务优化

对于业务处理来说,一般就是查表或者调用微服务

  • 数据库优化是一个冗长的议题,如果你使用Mybatis,可以参考我写的Gitbook相关文档
  • 微服务优化一般来说只能优化传输语法层面,比如将JSON换成二进制通信,更多可以使用Sleuth进行分析,可以参考我写的Gitbook中Eureka的相关文档
  • 对于频繁调用的可以考虑使用Redis/Elastic进行缓存计算结果

4. Summary

通过上述改造,项目终于又快起来了,说白了优化只是一个苦力活,这里工作应该尽可能包出去。

5. 附录

5.1. 如何区分生产环境

在本地开发时,一般开发者喜欢分散开的js文件,而上线后需要打包成一个单独的js文件,那么如何实现呢?

  • IDEA自动打包

最简单的操作就是使用上文的方法自动打包,这样就不用区分各种环境,缺点是打包速度比较慢。

  • 纯前台实现
1
2
3
4
5
6
7
8
<script>
if(window.Env.dev){
document.println('<script src="' + 'a.js' + '"<\/script>')
document.println('<script src="' + 'b.js' + '"<\/script>')
} else{
document.println('<script src="' + 'xxxx.all.min.js' + '"<\/script>')
}
</script>

这种方法的缺点就是很丑,但是js速度不会损失(如果是css的话肯定就慢了)

  • 后端模版引擎实现

同上,比如将当前的spring.profile.active作为上下文传到前台即可,这样看到渲染后的界面比较赏心悦目

5.2. Chrome的Audio工具

虽然网上有很多关于前端优化的文档,但是Chrome自带的这个工具已经非常好用了,通过将分数提高即可实现优化。