自JavaScript诞生以来,前端技术发展很是迅速。移动端白屏优化是前端界面体验的一个重要优化方向,Web 前端诞生了 SSR 、CSR、预渲染等技术。在美团支付的前端技术体系里,经过预渲染提高网页首帧优化,从而优化了白屏问题,提高用户体验,并造成了最佳实践。html
在前端渲染领域,主要有如下几种方式可供选择:前端
CSR | 预渲染 | SSR | 同构 | |
---|---|---|---|---|
优势 | 不依赖数据FP 时间最快客户端用户体验好内存数据共享 | 不依赖数据FCP 时间比 CSR 快客户端用户体验好内存数据共享 | SEO 友好首屏性能高,FMP 比 CSR 和预渲染快 | SEO 友好首屏性能高,FMP 比 CSR 和预渲染快客户端用户体验好内存数据共享客户端与服务端代码公用,开发效率高 |
缺点 | SEO 不友好FCP 、FMP 慢 | SEO 不友好FMP 慢 | 客户端数据共享成本高模板维护成本高 | Node 容易造成性能瓶颈 |
经过对比,同构方案集合 CSR 与 SSR 的优势,能够适用于大部分业务场景。但因为在同构的系统架构中,链接先后端的 Node 中间层处于核心链路,系统可用性的瓶颈就依赖于 Node ,一旦做为短板的 Node 挂了,整个服务都不可用。git
结合到咱们团队负责的支付业务场景里,因为支付业务追求极致的系统稳定性,服务不可用直接影响到客诉和资损,所以咱们采用浏览器端渲染的架构。在保证系统稳定性的前提下,还须要保障用户体验,因此采用了预渲染的方式。github
那么究竟什么是预渲染呢?什么是 FCP/FMP 呢?咱们先从最多见的 CSR 开始提及。web
以 Vue 举例,常见的 CSR 形式以下:chrome
一切看似很美好。然而,做为以用户体验为首要目标的咱们发现了一个体验问题:首屏白屏问题。typescript
浏览器渲染包含 HTML 解析、DOM 树构建、CSSOM 构建、JavaScript 解析、布局、绘制等等,大体以下图所示:npm
要搞清楚为何会有白屏,就须要利用这个理论基础来对实际项目进行具体分析。经过 DevTools 进行分析:后端
由此得出结论,由于要等待文件加载、CSSOM 构建、JS 解析等过程,而这些过程比较耗时,致使用户会长时间出于不可交互的首屏灰白屏状态,从而给用户一种网页很“慢”的感受。那么一个网页太“慢”,会形成什么影响呢?浏览器
Global Web Performance Matters for ecommerce的报告中指出:
咱们团队主要负责美团支付相关的业务,若是网站太慢会影响用户的支付体验,会形成客诉或资损。既然网站太“慢”会形成如此重要的影响,那要如何优化呢?
在User-centric Performance Metrics一文中,共提到了4个页面渲染的关键指标:
基于这个理论基础,再回过头来看看以前项目的实际表现:
可见在 FP 的灰白屏界面停留了很长时间,用户不清楚网站是否有在正常加载,用户体验不好。
试想:若是咱们能够将 FCP 或 FMP 完整的 HTML 文档提早到 FP 时机预渲染,用户看到页面框架,能感觉到页面正在加载而不是冷冰冰的灰白屏,那么用户更愿意等待页面加载完成,从而下降了流失率。而且这种改观在弱网环境下更明显。
经过对比 FP、FCP、FMP 这三个时期 DOM 的差别,发现区别在于:
仍然以 Vue 为例, 在其生命周期中,mounted 对应的是 FCP,updated 对应的是 FMP。那么具体应该使用哪一个生命周期的 HTML 结构呢?
mounted (FCP) | updated (FMP) | |
---|---|---|
缺点 | 只是视觉体验将 FCP 提早,实际的 TTI 时间变化不大 | 构建时须要获取数据,编译速度慢构建时与运行时的数据存在差别性有复杂交互的页面,仍需等待,实际的 TTI 时间变化不大 |
优势 | 不受数据影响,编译速度快 | 首屏体验好对于纯展现类型的页面,FP 与 TTI 时间近乎一致 |
经过以上的对比,最终选择在 mounted 时触发构建时预渲染。因为咱们采用的是 CSR 的架构,没有 Node 做为中间层,所以要实现 DOM 内容的预渲染,就须要在项目构建编译时完成对原始模板的更新替换。
至此,咱们明确了构建时预渲染的大致方案。
构建时预渲染流程:
因为 SPA 能够由多个路由构成,须要根据业务场景决定哪些路由须要用到预渲染。所以这里的配置文件主要是用于告知编译器须要进行预渲染的路由。
在咱们的系统架构里,脚手架是基于 Webpack 自研的,在此基础上能够自定义自动化构建任务和配置。
项目中主要是使用 TypeScript,利用 TS 的装饰器,咱们封装了统一的预渲染构建的钩子方法,从而只用一行代码便可完成构建时预渲染的触发。
装饰器:
使用:
从流程图上,须要在发布机上启动模拟的浏览器环境,并经过预渲染的事件钩子获取当前的页面内容,生成最终的 HTML 文件。
因为咱们在预渲染上的尝试比较早,当时尚未 Headless Chrome 、 Puppeteer、Prerender SPA Plugin等,所以在选型上使用的是 phantomjs-prebuilt(Prerender SPA Plugin 早期版本也是基于 phantomjs-prebuilt 实现的)。
经过 phantom 提供的 API 可得到当前 HTML,示例以下:
为了提升构建效率,并行对配置的多个页面或路由进行预渲染构建,保证在 5S 内便可完成构建,流程图以下:
理想很丰满,现实很骨感。在实际投产中,构建时预渲染方案遇到了一个问题。
咱们梳理一下简化后的项目上线过程:
开发 -> 编译 -> 上线
假设本次修改了静态文件中的一个 JS 文件,这个文件会经过 CDN 方式在 HTML 里引用,那么最终在 HTML 文档中的引用方式是 <script src="http://cdn.com/index.js"></script>
。然而因为项目尚未上线,因此其实经过完整 URL 的方式是获取不到这个文件的;而预渲染的构建又是在上线动做以前,因此问题就产生了:
构建时预渲染没法正常获取文件,致使编译报错
怎么办?
请求劫持
由于在作预渲染时,咱们使用启动了一个模拟的浏览器环境,根据 phantom 提供的 API,能够对发出的请求加以劫持,将获取 CDN 文件的请求劫持到本地,从而在根本上解决了这个问题。示例代码以下:
最终,构建时预渲染研发流程以下:
开发阶段:
发布阶段:
完整的用户请求路径以下:
经过构建时预渲染在项目中的使用,FCP 的时间相比以前减小了 75%。
寒阳,美团资深研发工程师,多年前端研发经历,负责美团支付钱包团队和美团支付前端基础技术。
咱们美团金融服务平台大前端研发组在高速成长中,咱们欢迎更多优秀的 Web 前端研发工程师加入,感兴趣的朋友能够将简历发送到邮箱:shanghanyang@meituan.com。