哔哩哔哩 Web 首页重构——回首2021
The following article is from 哔哩哔哩技术 Author 刘磊
本期作者
刘磊
高能链开放平台资深前端开发工程师
01 前言
在 2021 年时我们通过数据分析发现:在电脑端有越来越多用户的电脑屏幕切换成了大屏,现有 B 站的网页设计风格已经难以在宽屏设备上高效率的做内容分发,因此我们决定对 B 站网页版的整体视觉风格做一个大的更新,目标是能够在大屏和小屏上都能够对内容做到高效率的分发。
B站的网页端页面数量众多、业务逻辑复杂,因此我们决定先从用户感知最明显、且相对复杂的B站首页开始推进这个计划。
02 方案
用户:
因为B站的用户大多是年轻人,所以对于改版的接受能力较强,且用户软件版本较新,因此我们可以采用稍微激进一些的技术方案配合灰度控制来做改版。
部分小众用户有自定义B站主题的需求,因此在改版前要考虑他们 hack 时的体验。
产品:
以提高分发效率为目标,尝试提升内容屏占比 + 更深入的与 AI 推荐相结合做内容分发。
设计:
为了让整个网页保留适配窄屏(iPad)和更大屏幕(带鱼屏)的可能性,整个新版网页的设计语言是“栅格化 + 响应式”。
采用更扁平化的结构设计,弱化色彩,增强对内容的呈现;结构化页面元素,组件化设计,提升产品迭代效率。
研发:
JS:在 2021 年这个时期对 Web 项目进行大规模重构最好的技术选型是 NextJS(React)或 Vue3,在公司内部我们 Vue 用的较多,且 NextJS 更适合海外项目,因此我们决定使用 Vue3 作为基础框架。当时基于 Vue3 的 SSR 方案还在演化,ESR 的方案少有用例,再加上团队在过往的积累中对 SSR 更加熟练,因此我们决定基于 Vue3 框架自己实现一套 SSR 渲染的方案。(PS:体验上 CSR < SSR < ESR)
CSS:在做响应式和栅格化的技术方案选型时,可使用百分比宽度、calc() 计算宽度、JS 计算宽度等;在布局方案上,可选的有 flex、Grid 布局等;CSS 框架上可选的有 Sass、Less 等,最终我们为了实现一个更完美的页面布局,采用了@media + grid + Sass 的方案。(PS:Sass 使用率最高、Grid 是二维栅格布局、@media 是响应式必备)
多主题:在多主题支持上可选的方案有:在预编译阶做变量收集、DesignToken、CSS-var,最终我们选择了 CSS-var 的方案,是因为:预编译阶段的变量不利于通过浏览器插件注入变量的形式覆盖主题色,DesignToken 的形式过于灵活反而导致工程复杂度会变高和性能变差,而 CSS-var 在现阶段的可用性相对来说是最好的。
组件化:组件化方案我们采用了 monorepo/Lerna 来管理仓库,把项目拆分成了 PureJS dependencies → Framework(UI) dependencies → Service dependencies → Project/Page,具象化表达就是指:http.ts → VideoCard.vue → BiliHeader.umd.js → Homepage.vue
总结:最终我们的技术方案是:Lerna + Vite + Vue3(SSR、ts、setup)+ Grid + @media + Sass + CSS-var
03 难点
在整个项目的开发过程中从工程层面去解构重难点,其中值得与大家分享的点有:
如何抽象页面的栅格化/响应式,做到可复用、可拆解
如何以最低成本实现一套 Vue3 SSR/CSR 同构方案,支持页面降级
如何实现一套 API → Store → View(List → Item) 的高可复用模式
如何让网页兼顾用户体验、可维护性、性能
接下来我们针对上述问题逐一做解答。
3.1 如何抽象页面的栅格化/响应式
做到可复用、可拆解
得益于我们使用 Grid + @media 来做响应式和栅格化这里不存在特殊的难点,难点在于下面这个场景:
产品要求:在不同屏幕尺寸下导航栏区域展示的分区个数不同,且要求永远只把最后几项收录到“更多”里。
难点分析:因为首页是 SSR 的且导航栏在第一屏,所以我们需要在页面渲染时就展示出正确的数据,那么它必定是使用 CSS 实现的逻辑而非 JS 去计算。那么我们要实现的就是:通过 CSS 在不同屏幕尺寸下把一个列表的最后几项隐藏或展示。我们的实现方案是:
核心的代码逻辑就是借助 CSS 的 nth-of-type 选择器 + Sass mixin 来实现,我们对该逻辑进一步封装,得到:
最终我们在导航栏这里的布局代码是:
为了能够更加灵活的适配各个页面的布局场景,因此我们对 Sass mixin 的拆分比较原子化,在代码中就会出现有抽象程度较高的 @include 调用,但整体上只要对 Grid 布局和 Sass 语法足够熟练就快速掌握,提升开发效率。
3.2 如何以最低成本实现一套
Vue3 SSR/CSR 同构方案支持
页面降级
在 Vue2 框架里主流的 SSR 实现方案是借助 Vuex,主要流程是调用 store.replaceState() 函数进行 hydrate(store.replaceState 把 window 下序列化后的数据再反序列化,重新回归到 store 中;页面/组件依赖的数据从 store 中获取,以此完成 hydrate)。
所以这里的核心点在于:我们只需要在 client 让 Vue 框架的数据从本地(window)获取而非服务端获取即可,在常见的技术语境中我们称这个过程为 —— 重放。我们在服务端获取到的数据存储在 window 下,当客户端再次初始化的时候会重新发起请求,这个时候的请求结果从本地获取即可。因此我们要做的就是:封装出一个 http.ts 的 PureJS dependencies,让它能够实现同构(重放)。
同时,因为 http.ts 是支持同构的,所以哪怕 SSR 服务挂了直接降级,页面也可以以 CSR 的方式正常渲染。
3.3 如何实现一套 API → Store
→ View(List→Item)的高可复
用模式
B站的首页有众多楼层,一些楼层虽然从视觉上差异不大,但是业务方却不同,因此会涉及到数据格式规范化的问题。
在常规的架构中,我们会在前端代码里抽象出 API 层来实现数据缓存、数据转换、分页控制等,然后把数据导入 Store 中再分发到页面和组件里。但是 B站首页的特点在于:各个模块之间的数据基本没有交互,因此并不需要一个中心化的 Store 来聚合状态,我们要做的模型是 API → Component(List) → Component(Item),在 API 层处理好数据差异的前提下,只要 List 抽象的足够好,Item 的编写也会很简单,达到模块化、可组合的效果。
这里的实现不会过多的着墨,可以参考:这个库的实现[1]。
3.4 如何兼顾用户体验、
可维护性、性能
在上文中我们讲到了自己实现 Vue SSR 的逻辑,熟练 Vue SSR 的读者一定对 asyncData 这个函数非常熟悉,它在 Vue2 里面只能应用于 page-component,导致项目的可维护性变低(参考:如何评价 React Server Component.)
本质原因是:如果允许非页面组件内调用 asyncData 就会导致整个网络请求是串行的,而放在 page-component 去统一调用那就是并行的。React 的解决方案是 React Server Component,我们在使用 Vue3 重构B站首页时采用了一种另类的方法 —— 服务端重放。在项目中,每个组件所需的数据仍然在组件中去发起请求,在页面首次渲染时请求会是串行模式,然后我们会在 http.ts 里面把当前页面的 Request 收集起来,在下一次该页面再访问的时候,重放这些 Request 即可获得最新的 Response。
传统的缓存策略是缓存 Response,这样会导致在千人千面场景下遇到数据不可复用的问题和数据时效性问题,但当我们去存储 Request 时,在绝大多数场景下都是能够顺利工作的。
解决完一些技术难题后,我们的页面顺利的搭建起来了,那么最后它的性能是什么表现呢:
第一次跑分的时候只有22分,也就是说我们还有70+分的优化空间,接下来我就给大家分享以下 PC 新首页的优化思路。
通过 Vue3 SSR 的同构 hydrate 和 http.ts 的重放方案,我们已经解决了可维护性和用户体验的问题,接下来我们主要去解决性能问题。
3.5 性能优化原则
1.能够使用 CSS 解决的问题就不要使用 JS;
2.需要掌握的知识有:学会使用 chrome devtools 的 performance 面板,查看火焰图;了解 reflow、repaint;
3.性能优化场景:首屏、静置、交互;
4.性能优化原则:
3.6 性能优化实践
1.使用 CSS 实现特殊文字顶格效果(能使用 CSS 就不用 JS)
2.使用 CSS 代替 Lottie 动画绘制轮播图特效(静置场景的优化)
3.使用 setTimeout + requestIdelCallback 加载首屏内嵌的 iframe 页面(首屏场景 + 优化执行顺序)
4.外部 SDK 延迟加载(script defer),并在调用时 retry(首屏场景 + 异步化)
5.在页面滚动时使用 IntersectionObserver 代替 getBoundingClientRect 判断内容与视口的交叉关系(互动场景 + 避免 reflow)
6.能单例化/可复用的场景就适当复用 + 延迟销毁(首页的 Popover 组件)
7.针对特定指标的优化,如:为了避免发生 CLS 我们给所有的组件都加了相同尺寸的骨架屏
以上提到的只是整个优化过程中的案例,基于性能优化的原则然后秉着“哪怕芝麻也是肉”的心态去一步一步的优化,最终首页的性能指标是:
(PS:LCP 无法优化是因为首屏的一些图片对清晰度的要求较高,所以没有对它做进一步的优化)
04 结语
哔哩哔哩 PC Web 在2021年年初的时间点使用了较为激进的技术改造方案,一方面是出于对性能的考量,另一方面是不想为后人留下“技术债”。虽然整个开发过程中要克服很多困难(开源生态不完美、最佳实践没参考),但整体上还是达到了我自己非常满意的一个水准。在工程化、组件化、性能上都相较于以往的代码有一定的进步,也为后续其它页面的改造留下了一些实践参考,这个过程中离不开团队的努力和上级对我们技术选型方案的支持,这是一段非常不错的 coding 经历。
站在2022年末的当下来回顾当初的技术选型,B站在 PC Web 端重构时表现出来的技术水平(技术选型的先进性 + 解决方案的先进性 + 执行结果的高效性)在国内各大互联网 ToC 业务场景下也是领先的(常态下性能:Vue2 < React < Vue3,目前国内各大视频垂类互联网公司,只有B站在 ToC 主要业务场景使用了 Vue3,数据来源:框架采用分布,框架性能对比)。
参考链接:
[1]https://github.com/flowlist/vue-listview
参考阅读:
本文由高可用架构转载。技术原创及架构实践文章,欢迎通过公众号菜单「联系我们」进行投稿