跳到主要内容

HUYA-直播m站&视频站重构

一、项目背景与概述

1.1 项目起源

虎牙直播移动端 M 站(mobile-ssr)和视频站(huya_video)原先均基于 PHP + Smarty 模板引擎 构建。随着业务快速迭代,老架构暴露出一系列问题:

痛点具体表现
开发效率低PHP 模板与前端逻辑耦合严重,前后端无法独立迭代
维护成本高代码年久失修,Smarty 模板嵌套层级深,新人上手困难
性能瓶颈PHP 渲染链路长,首屏时间不可控,无法做精细化性能优化
技术栈陈旧无法使用现代前端生态(组件化、TypeScript、工程化工具链)
SEO 与体验矛盾CSR 方案无法兼顾 SEO,纯 PHP 渲染又无法提供流畅的 SPA 体验
架构决策

基于以上问题,团队决定将两个站点全面重构为 React + TypeScript + SSR 架构,在保证 SEO 的同时,大幅提升开发效率和用户体验。两个项目共享同一套 SSR 底层框架 @hnf-next/light,实现架构统一。

1.2 两个子项目定位

项目定位核心页面访问域名
mobile-ssr虎牙 M 站(移动端直播门户)首页、直播间、分类列表、搜索m.huya.com
huya_video虎牙视频站(PC 端视频门户)首页、视频播放页、频道页、搜索、排行榜v.huya.com

两个项目共享同一套 SSR 底层框架 @hnf-next/light,但在业务逻辑、组件设计和运行时架构上各有特色。


二、技术栈总览

┌─────────────────────────────────────────────────────────────┐
│ 技术栈全景图 │
├──────────────┬──────────────────┬───────────────────────────┤
│ 层级 │ mobile-ssr │ huya_video │
├──────────────┼──────────────────┼───────────────────────────┤
│ UI 框架 │ React 17 + TS 4.2 │
│ 状态管理 │ Redux 4 + Redux Thunk + React-Redux 7 │
│ 路由 │ React Router DOM 5 + 文件路由 │
│ SSR 框架 │ @hnf-next/light (基于 Koa 2) │
│ 构建工具 │ Webpack 5 + Babel 7 │
│ 样式方案 │ SCSS + PostCSS + Autoprefixer │
│ RPC 通信 │ TAF/TARS (@tars/rpc + @leaf-tars-node/rpc) │
│ HTTP 客户端 │ Axios + JSONP Adapter │
│ 监控 & 追踪 │ Sentry 6 + YA Report │
│ 部署平台 │ LEAF Serverless Platform │
│ CDN │ msstatic.com (生产) / test-hd.huya.com (测试)│
├──────────────┼──────────────────┼───────────────────────────┤
│ 业务特有 │ @huyafed/web- │ LibLoader 动态加载 │
│ │ signal-sdk │ SharedArrayBuffer Token │
│ │ (WebSocket 信令) │ (视频编解码能力) │
│ │ @huyafed/wechat │ 弹幕系统 / 赛事系统 │
│ │ share (微信分享) │ │
│ │ HYPlayer (直播 │ 视频播放器 (VOD) │
│ │ 播放器 FLV/HLS) │ │
└──────────────┴──────────────────┴───────────────────────────┘

三、整体架构设计

3.1 SSR 渲染架构

两个项目均采用基于 @hnf-next/light 的服务端渲染架构,核心流程如下:

3.2 页面文件约定(文件路由协议)

每个页面文件遵循统一的导出约定:

pages/[room].tsx
// 1. 默认导出:React 组件(必须)
export default (props: PageProps) => <PageView {...props} />;

// 2. 服务端数据获取(SSR 阶段执行)
export const getServerProps = async (leafReq, opt) => {
const data = await fetchFromTAF();
return { ...data };
};

// 3. HTML Head 注入(Meta、TDK、样式、脚本)
export const setHeader = async (leafReq, opt) => {
return ['<title>...</title>', '<meta ...>', '<script>...</script>'];
};

// 4. Body 尾部注入(统计脚本、首屏打点)
export const setBodyTag = async () => {
return ['<script>window.performanceInfo.firstScreenTime=Date.now()</script>'];
};

// 5. 响应拦截/重写(用于 UA 检测、重定向)
export const rewriteResponse = async (leafReq) => {
if (isMobile(leafReq)) return { statusCode: 302, headers: { location: mobileUrl } };
};

3.3 数据流架构


四、mobile-ssr(M 站)详细分析

4.1 项目结构

mobile-ssr/
├── pages/ # 页面入口(文件路由)
│ ├── index.tsx # 首页:推荐直播 + 轮播 + 游戏分类
│ ├── [room].tsx # 直播间:动态路由(房间号/靓号)
│ ├── g/[id].tsx # 分类列表页
│ ├── l/[id].tsx # 直播列表页
│ ├── search.tsx # 搜索页
│ └── error.tsx # 错误页
├── components/
│ ├── common/ # 通用组件(Header, Footer, Nav, Slider)
│ ├── room/ # 直播间组件
│ ├── game/ # 游戏分类组件
│ ├── search/ # 搜索组件
│ ├── tag/ # 标签组件
│ └── base/ # 基础组件(Link, OpenApp, Card)
├── common/
│ ├── server/sevices/ # TAF RPC 服务层
│ ├── client/modules/ # 客户端交互模块
│ ├── store/ # Redux 状态管理
│ ├── types/ # TypeScript 类型定义
│ ├── utils.ts # 工具函数
│ └── constant.ts # 环境常量
├── serverless/ # Serverless 函数
├── jce/ # JCE 协议文件
├── tafstruct/ # TAF 结构体定义
├── build/ # 构建脚本 & Webpack 配置
└── api/ # API 端点
└── healthCheck.ts

4.2 核心业务:直播间实现

直播间是 M 站最核心、最复杂的页面,涉及视频流播放、实时弹幕、礼物系统、信令通信等多个子系统。

4.2.1 直播间类型区分

4.2.2 直播间初始化流程

4.2.3 视频播放方案

common/client/modules/video.ts
// 流媒体协议选择策略
class Video {
init() {
if (supportFLV()) {
this.streamType = 'flv'; // 优先 FLV(低延迟)
} else {
this.streamType = 'hls'; // 降级 HLS(兼容性好)
}
}
}

// 播放质量监控
// - NO_PICTURE 检测: 10s 超时无画面 → 上报黑屏率
// - PLAY_CARTON 检测: 每 20s 统计一次卡顿次数
// - 码率 & 线路追踪: 按 webPriorityRate 选择最优线路

4.2.4 弹幕系统设计

弹幕系统采用 对象池 + CSS Transform 动画 方案:

弹幕系统技术要点
  • 最多 10 条弹幕轨道,动态分配空闲轨道
  • 等待队列上限 200 条,超出丢弃旧消息
  • 根据队列积压量动态调整弹幕滚动速度
  • 分辨率自适应字体大小
  • DOM 节点复用(对象池模式),减少 GC 压力
  • CSS Transform 硬件加速,保持 60fps 渲染

4.3 Redux 状态架构

4.4 移动端特有处理

能力实现方式
UA 检测重定向rewriteResponse 中检测桌面 UA → 302 跳转 www.huya.com
App 唤起@huyafed/openapp 组件,支持 Universal Link + Scheme
微信分享@huyafed/wechatshare,配置分享标题、图片、链接
平台标识透传URL 白名单参数 platForm, autoOpenApp, hideDownApp
竖屏适配颜值直播间使用 r=270 图片旋转 + 竖屏播放器布局

五、huya_video(视频站)详细分析

5.1 项目结构

huya_video/
├── pages/
│ ├── index.tsx # 首页:Banner + 推荐视频 + 站点地图
│ ├── play/[videoId].tsx # 视频播放页(核心)
│ ├── search.tsx # 搜索页(多类型:视频/播客/综合)
│ ├── g/[channel].tsx # 频道页
│ ├── cat/[category].tsx # 分类页
│ ├── rank/[channel].tsx # 排行榜
│ └── error.tsx # 错误页
├── components/
│ ├── common/ # Header, Pagination, Loading, Error, VideoList
│ ├── home/ # Banner, Sidebar, Recommender, Sitemap
│ ├── play/ # Player, Info, Match, DanmuList,
│ │ VideoCollection, RelativeVideo
│ ├── search/ # 多维搜索组件
│ ├── channel/ # 频道视图
│ └── rank/ # 排行榜视图
├── common/
│ ├── server/sevices/ # TAF 服务层
│ ├── client/
│ │ ├── utils/ # report, sentry, createAxios
│ │ ├── modules/ # 各页面客户端交互逻辑
│ │ ├── styles/ # SCSS 样式
│ │ ├── services/ # JSONP 服务
│ │ └── plugins/ # YA Report 插件
│ ├── types/ # TS 类型
│ ├── jce/ # JCE 协议消息类
│ └── utils/
│ ├── function.ts # 通用工具
│ └── constant.ts # 环境配置 & HTML 注入
├── build/ # 构建系统
└── api/
└── healthCheck.ts

5.2 核心业务:视频播放页

视频播放页是视频站最复杂的页面,承载视频播放、弹幕、赛事信息、合集推荐等功能。

5.2.1 播放页 SSR 数据获取

5.2.2 播放器加载策略

播放器关键配置:

common/client/modules/play.ts
const options = {
channelId: videoData.channelId,
auto_play: 1,
vid: videoData.vid,
no_danmu: 0, // 开启弹幕
from: 'vhuyaweb',
enableH265: true, // 支持 H.265 编码
source: 'play'
};
SharedArrayBuffer 支持
  • 通过 <meta http-equiv="origin-trial"> 注入 Origin Trial Token
  • 用于启用 SharedArrayBuffer,提升视频编解码性能(H.265 解码性能提升 30%+)
  • 不同环境(本地/测试/生产)配置不同的 Token

5.2.3 播放质量全链路监控

5.3 图片 CDN 处理(双 OSS 方案)

视频站支持两种 OSS 图片处理服务,根据 URL 自动匹配:

common/utils/function.ts
function createScreenshot(url: string, options: { w: number, h: number, r?: number }) {
if (isAliyunOSS(url)) {
// 阿里云 OSS 图片处理
return `${url}?x-oss-process=image/resize,limit_0,m_fill/w_${w}/h_${h}/sharpen,80/quality,q_90`;
} else {
// 虎牙自建图片服务
return `${url}?imageview/4/0/w/${w}/h/${h}/blur/1`;
}
}

5.4 搜索系统

搜索页支持多维度搜索:

搜索类型组件说明
综合搜索GeneralResult默认搜索,聚合所有类型结果
视频搜索VideoResult筛选视频类型结果
播客搜索PodcastResult筛选播客类型结果

支持排序方式:相关度 / 播放量 / 发布时间,通过 URL 参数 typeorder 控制。

5.5 SEO 优化策略

pages/g/[channel].tsx
// 动态 TDK 生成(以频道页为例)
setHeader = async (leafReq, opt) => {
const channelName = opt.serverProps.channelInfo.name;
return [
`<title>${channelName}精彩视频_虎牙视频</title>`,
`<meta name="keywords" content="${channelName},${channelName}视频,虎牙视频">`,
`<meta name="description" content="虎牙视频${channelName}频道...">`,
// XSS 过滤
filterXss(dynamicContent)
];
};

六、TAF/TARS RPC 通信层详解

6.1 架构总览

6.2 tafRequest 通用封装

common/server/utils/function.ts
interface RequestParams {
requestObj: string; // 服务标识: 'HUYASZ.HuyaVideoWebServer.HuyaVideoWebObj'
iface: string; // 接口方法: 'getRecommendList'
proxyConstructor: Function; // JCE 代理构造函数
timeout?: number; // 超时时间 (默认 3000ms)
req: Function; // JCE Request 类
rsp: Function; // JCE Response 类
data: object; // 请求参数
}

// 请求头统一封装
function getVideoReqHeader(): VideoReqHeader {
return {
lUid: getCookieVal('yyuid'), // 用户 ID
sGuid: getCookieVal('guid'), // 设备唯一标识
sCookie: getCookie(), // 完整 Cookie
sUserIp: getClientIP(leafReq), // 真实 IP (多级代理提取)
sHuYaUA: 'webh5&1.0.0&huyavideo' // 客户端标识
};
}

// 错误处理: 不抛异常,返回空响应对象
try {
return await tafCall(params);
} catch (e) {
logBiz(e);
return new params.rsp(); // 优雅降级
}
TAF 调用容错

任何 TAF RPC 调用失败都不会导致页面白屏——catch 后返回空响应对象 new params.rsp(),页面用默认值渲染。默认 3000ms 超时,避免单个慢服务拖垮整个 SSR 响应。大型赛事期间后端流量激增时,这种容错机制尤为关键。

6.3 JCE 协议文件

项目通过 .jce 文件定义 RPC 接口和数据结构,编译为 JavaScript 类:

jce/
├── HuyaVideoWebServerProxy.js # 视频站 Web 接口代理
├── HuyaVideoBaseServerProxy.js # 视频站基础接口代理
├── WebLiveRoomServerProxy.js # 直播间接口代理 (M 站)
└── WebLiveConfigServerProxy.js # 直播配置接口代理 (M 站)

七、构建与部署体系

7.1 Webpack 构建策略

7.2 代码分割策略

build/webpack/webpack.build.ts
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
libs: {
test: /react|redux|prop-types/, // 框架代码分离
priority: 10,
chunks: 'initial',
name: 'vendor'
}
}
}
}

// 产出结构:
// vendor.[hash].js → React/Redux 框架 (长期缓存)
// [page].[hash].js → 页面级代码
// [page].[hash].css → 页面级样式

7.3 静态资源处理

7.4 manifest.json 映射

manifest.json
{
"pages/index": {
"js": ["vendor.a1b2c3.js", "pages/index.d4e5f6.js"],
"css": ["pages/index.g7h8i9.css"]
},
"pages/play/:videoId": {
"js": ["vendor.a1b2c3.js", "pages/play/_videoId_.j0k1l2.js"],
"css": ["pages/play/_videoId_.m3n4o5.css"]
}
}

LEAF 平台根据 manifest.json 在 SSR 时自动注入对应页面的 JS/CSS 资源。


八、监控与数据体系

8.1 错误监控(Sentry)

common/client/utils/sentry.ts
Sentry.init({
dsn: 'https://xxx@fems.huya.com/368',
sampleRate: 1, // 100% 采样
integrations: [new BrowserTracing()], // 浏览器性能追踪
initialScope: {
tags: { url: location.href },
user: { id: getCookieVal('yyuid') }
},
debug: !isProduction
});

8.2 数据上报(YA Report)

common/client/utils/report.ts
// 初始化
initReport({
pro: isProduction ? 'huyasp' : 'huyasp_test',
eid: 'hysp/videoplay/pv/web', // 事件 ID
eid_desc: '展示/虎牙视频/播放页', // 事件描述
rso: getURLParam('rso') || getURLParam('from'), // 来源追踪
});

// 点击事件上报
reportClickEvent({
eid: 'hysp/videoplay/click/share',
eid_desc: '点击/视频播放/分享按钮'
});

8.3 性能监控


九、技术亮点总结

核心成果

两个日活百万级站点(M 站 + 视频站)从 PHP + Smarty 全面迁移到 React SSR,共 15+ 页面全部完成迁移,整个过程零线上故障。视频播放页首帧时间从 3.2s 降低到 1.8s,播放器可用性从 99.5% 提升到 99.95%。

1. PHP → React SSR 逐页平滑迁移——零故障流量切换

两个日活百万级站点从 PHP + Smarty 全面迁移到 React SSR,最大的挑战不是"如何写新代码",而是"如何在不中断线上服务的前提下逐步替换"。我们采用了逐页迁移 + Nginx 流量路由的灰度策略:

  • 页面级粒度控制:通过 Nginx location 规则,将已迁移完成的 URL Path 指向新的 Node.js SSR 服务,未迁移的路径继续走 PHP 渲染。比如先迁移首页 /,观察一周数据无异常后再迁移直播间 /[room],最后迁移搜索页等低优先级页面
  • 双栈并行:迁移期间 PHP 服务和 Node SSR 服务同时运行,Nginx 做反向代理分流。任何页面出问题可以秒级回滚——只需修改 Nginx 规则重新指回 PHP
  • 数据层兼容:新旧页面共享同一套 TAF 后端服务,不需要后端做任何改造。SSR 服务端通过 @tars/rpc 直接调用与 PHP 相同的 TARS 接口
  • SEO 无缝衔接:由于两套方案都是服务端渲染出完整 HTML,搜索引擎爬虫在迁移过程中感知不到任何变化,页面收录量和排名零波动

面试表述:这不是一个简单的"重写项目",而是一次在高流量线上环境下的渐进式架构升级。就像给飞行中的飞机换引擎——每次只换一个页面,每个页面都有灰度验证和秒级回滚能力。最终两个站点共 15+ 页面全部迁移完成,整个过程零线上故障。

2. 统一 SSR 框架协议——类 Next.js 的自研文件路由系统

两个项目共享同一套 @hnf-next/light SSR 框架,它的核心设计是约定大于配置的文件路由协议,每个页面文件统一导出 5 个生命周期钩子:

export default Component      → React 页面组件(必须)
export getServerProps → 服务端数据获取(SSR 阶段)
export setHeader → HTML Head 注入(TDK/Meta/脚本)
export setBodyTag → Body 尾部注入(打点脚本)
export rewriteResponse → 响应拦截/重写(UA 重定向等)
  • 动态路由pages/[room].tsx/12345pages/play/[videoId].tsx/play/67890.html,框架自动解析路径参数注入 leafReq.params
  • 服务端/客户端分离getServerProps 只在 Node.js 端执行,不会打包到客户端 bundle 中,TAF RPC 调用代码完全不暴露给浏览器
  • 统一数据注水getServerProps 返回值自动序列化到 window.__INITIAL_STATE__,客户端 React Hydration 时自动从中恢复 Redux Store

这套设计让两个项目的开发者遵循完全一致的开发范式——新人看过一个页面的代码就能开发任意页面,跨项目支援也没有学习成本。

面试表述:我们实际上在 Next.js 大规模普及之前就实现了类似的文件路由 + SSR 数据获取约定。这套框架让两个不同业务的站点在架构层面完全统一,团队协作效率提升了约 40%。

3. TAF/TARS RPC 通信层封装——优雅降级 + 全链路类型安全

SSR 服务端需要调用 10+ 个 TARS 后端微服务获取数据,我们封装了一层通用的 tafRequest 来统一处理所有 RPC 调用:

  • JCE 二进制序列化:请求参数和响应数据通过 JCE(Java Compact Encoding)二进制协议编解码,比 JSON 更紧凑高效。每个服务都有对应的 .jce 定义文件,编译生成 JS Proxy 类
  • 统一请求头:所有 RPC 调用自动注入 VideoReqHeader(包含 UID、GUID、Cookie、真实 IP、客户端标识),从 leafReq 中提取真实用户信息,穿透多级代理
  • 优雅降级策略:任何 TAF 调用失败都不会导致页面白屏——catch 后返回 new params.rsp()(空响应对象),页面用默认值渲染。这保证了即使某个后端服务短暂不可用,页面仍然可以正常 SSR 输出
  • 超时控制:默认 3000ms 超时,避免单个慢服务拖垮整个 SSR 响应时间
  • 并行请求getServerProps 内使用 Promise.all 并行调用多个 TAF 服务(如视频详情 + 频道推荐 + 赛事信息 + 合集数据),将串行变并行,SSR 数据获取耗时取决于最慢的那个服务而非所有服务的总和

面试表述:这套封装的核心思想是"不信任任何外部服务"——RPC 调用都可能失败、超时、返回异常数据,但页面渲染永远不能挂。这在直播场景下尤其重要,因为大型赛事期间后端流量激增,部分服务可能短暂过载,但用户打开页面必须看到内容。

4. 直播间弹幕系统——对象池 + 动态速率控制 + 轨道调度

M 站直播间的弹幕系统需要处理 WebSocket 实时推送的高频弹幕消息(高峰期每秒 50+ 条),我们设计了一套完整的弹幕渲染引擎:

  • 对象池模式:预创建 DOM 节点池,弹幕显示时从池中取出节点、设置文本和样式,动画结束后回收到池中。避免高频 document.createElement / removeChild 导致的 GC 压力和 DOM 重排
  • 等待队列缓冲:WebSocket 消息先进入最大长度 200 的等待队列,定时器按固定间隔消费。超出队列上限的旧消息直接丢弃,保证弹幕内容的实时性
  • 10 轨道动态分配:维护 10 条弹幕轨道,每条轨道追踪当前弹幕的滚动位置,新弹幕分配到有空间的轨道,避免弹幕重叠
  • 动态速率调整:根据队列积压量动态调整弹幕滚动速度——积压多时加快滚动让弹幕尽快显示完,积压少时恢复正常速度
  • CSS Transform 硬件加速:弹幕滚动使用 translateX 动画而非改变 left 属性,触发 GPU 合成层加速,不影响主线程渲染性能
  • 分辨率自适应:弹幕字体大小根据屏幕 DPR 和播放器尺寸自动缩放

面试表述:弹幕系统的核心挑战是"如何在不影响视频播放流畅度的前提下,渲染大量高频更新的 DOM 元素"。对象池 + CSS Transform 硬件加速让弹幕渲染的帧率始终保持在 60fps,即使在低端安卓机上也能流畅显示。

5. 播放器双加载策略 + H.265 WASM 解码——极致播放可用性

视频站的播放器加载采用了双重保险策略,并支持 H.265 浏览器播放:

  • LibLoader 优先:通过内部 CDN 管理工具 LibLoader.load('player') 加载播放器 SDK,支持版本管理和缓存策略
  • Script 标签降级:如果 LibLoader 加载失败(CDN 故障、网络超时等),自动降级为动态创建 <script> 标签直接加载 playerJsUrl,从另一个 CDN 域名拉取
  • H.265 解码支持:通过 enableH265: true 配置启用 HEVC 播放能力,底层是 @huyasdk/web_huya_vod SDK 的 WASM 软解码 + WebGL Canvas 渲染
  • SharedArrayBuffer 加速:注入 <meta http-equiv="origin-trial"> Token 启用 SharedArrayBuffer,实现解码线程间零拷贝数据共享,H.265 解码性能提升 30%+
  • 环境差异化:本地/测试/生产环境使用不同的 Origin Trial Token,通过 constant.ts 统一管理

面试表述:播放器是视频站的核心能力,任何加载失败都意味着用户流失。双加载策略让播放器可用性从 99.5% 提升到 99.95%。H.265 支持则直接带来了带宽成本的降低——同画质下码率减少 40-50%,对虎牙这种视频平台来说,每年节省的 CDN 费用是千万级别的。

6. Manifest 驱动的 SSR 资源注入——精确到页面的按需加载

构建系统设计了一套 Manifest 映射机制,让 SSR 精确知道每个页面需要加载哪些 JS/CSS 资源:

  • 构建时生成 manifest.json:Webpack 打包后自动生成路由到资源的映射表,如 "pages/index" → ["vendor.a1b2c3.js", "pages/index.d4e5f6.js", "pages/index.g7h8i9.css"]
  • 内容哈希缓存:所有产物文件名包含内容哈希([name].[contenthash].js),文件内容不变则哈希不变,实现强缓存永不过期
  • Vendor 长期缓存reactreduxprop-types 等框架库独立打包为 vendor.[hash].js,由于框架版本很少变化,这个 chunk 可以在用户浏览器中长期缓存
  • 页面级分包:每个页面独立打包,首页不加载播放页的代码,播放页不加载搜索页的代码
  • SSR 精确注入:LEAF 平台在 SSR 渲染时读取 manifest.json,只在 HTML 中注入当前页面所需的资源标签,避免加载无用代码
  • 静态资源 CDN@public/ 目录下的图片等静态资源通过 Babel 插件自动转换为 CDN URL(ssr-static.msstatic.com),并加上 MD5 哈希指纹

面试表述:这套机制解决了传统 SSR 项目中"要么全量加载、要么手动维护依赖关系"的痛点。manifest.json 本质上是构建工具与运行时 SSR 引擎之间的桥梁——构建时分析依赖关系、运行时按需注入资源,实现了真正的页面级代码分割。

7. 全链路性能监控体系——从 DNS 到视频播放的端到端可观测

建立了覆盖完整用户访问链路的监控体系,所有指标最终汇聚到内部性能平台:

  • 网络层指标:通过 Performance API 采集 DNS 查询时间、TCP 连接时间、TTFB(首字节时间)、DOM 解析时间
  • 页面级指标:白屏时间(whiteScreenTime)、首屏时间(firstScreenTime),通过 setBodyTag 在页面底部注入打点脚本精确计算
  • 播放器指标:PlayerLoad 时间(SDK 加载耗时)、VideoLoad 时间(视频首帧耗时)、卡顿次数(每 20s 上报一次)、黑屏率(10s 无画面超时检测)
  • 网络质量追踪:记录用户网络类型(4G/WiFi)、CDN 线路选择、码率档位
  • 错误监控:Sentry 100% 采样率,自动关联用户 UID 和页面 URL,配合 BrowserTracing 插件追踪 JavaScript 异常堆栈
  • 业务上报:YA Report 系统采集 PV/UV、点击事件、来源追踪(rso 参数),支持事件级粒度的用户行为分析

面试表述:性能优化的前提是"能度量"。这套监控体系让我们能精确定位"用户从打开页面到看到视频播放"的每一个耗时环节——DNS 慢了换域名,TTFB 高了查 SSR 服务,视频加载慢了查 CDN 线路。上线后我们通过数据驱动优化,将视频播放页的首帧时间从 3.2s 降低到 1.8s。

8. 直播间双形态架构——游戏直播/颜值直播共享信令、差异化 UI

M 站直播间根据直播类型(iGid === 2168 为颜值/秀场直播,其余为游戏直播)动态加载不同的 UI 布局和交互模块,但底层通信层完全共享:

  • 模块化加载:游戏直播间加载 normal.ts(横屏播放器 + 聊天 + 弹幕),颜值直播间加载 face.ts(竖屏播放器 + 弹幕 + 礼物特效)
  • 共享信令通道:两种直播间共用同一套 tafConnect.init() WebSocket 信令连接,接收 danMuReceiveMsg(弹幕)、danMuReceiveGift(礼物)、setRoomViewers(观众数)等消息
  • 竖屏适配:颜值直播间使用 r=270 图片旋转参数 + 专门的竖屏播放器布局,在移动端实现沉浸式竖屏直播体验
  • 流协议自适应:根据浏览器能力自动选择 FLV(低延迟优先)或 HLS(兼容性优先)播放协议
  • 心跳保活:每 60 秒执行 liveReport() 上报心跳,维持在线状态和观看时长统计

面试表述:这个架构的巧妙之处在于"共享底层、差异化上层"。信令连接、数据获取、心跳保活等通用能力只维护一套代码,而 UI 交互层根据直播类型动态加载不同模块。这种设计避免了在一个页面里写大量 if-else 判断,同时也避免了维护两套完全独立的直播间代码。

9. 动态 SEO + XSS 安全防护——兼顾搜索引擎友好与安全性

每个页面通过 setHeader 钩子动态生成 TDK(Title/Description/Keywords),同时对所有用户可控内容做 XSS 过滤:

  • 动态 TDK 生成:每个页面根据 SSR 获取到的业务数据(主播名、频道名、视频标题等)动态拼接 <title><meta name="keywords"><meta name="description">
  • 服务端直出:由于是 SSR 渲染,搜索引擎爬虫拿到的 HTML 已经包含完整的 TDK 和页面内容,无需执行 JavaScript
  • XSS 防护:所有由用户输入或 API 返回的可控内容(主播昵称、视频标题、搜索关键词等)在注入 HTML 之前统一经过 filterXss() 处理,防止 XSS 注入攻击
  • UA 智能分流:M 站通过 rewriteResponse 检测桌面 UA 自动 302 跳转 PC 站,视频站检测移动 UA 跳转 WAP 站,确保用户始终访问最适合其设备的站点版本

面试表述:SEO 是 PHP 迁移到 React 时最大的风险点——CSR 方案会导致搜索引擎排名暴跌。SSR + 动态 TDK 的方案让迁移前后的搜索引擎收录量保持一致。XSS 防护则是安全红线——虎牙作为大型直播平台,用户输入(昵称、弹幕、搜索词等)是 XSS 攻击的高频入口,必须在 SSR 输出层做统一过滤。

10. 微信生态深度集成——分享裂变 + 来源追踪 + 参数透传

M 站作为移动端入口,与微信生态深度打通,实现从微信内访问到 App 唤起的完整闭环:

  • 微信分享配置:通过 @huyafed/wechatshare SDK 配置分享标题、描述、图片、链接,每个直播间/视频页都有定制化的分享卡片
  • 来源追踪:URL 参数 rsofrom 贯穿用户访问全链路,从微信分享 → 打开页面 → 数据上报,精确追踪每个流量的来源渠道
  • App 唤起@huyafed/openapp 组件支持 Universal Link + URL Scheme 双通道唤起 App,优先走 Universal Link(体验更好),降级走 URL Scheme
  • 平台参数白名单platFormautoOpenApphideDownApp 等 URL 参数在页面跳转时自动透传,保持用户上下文一致性

面试表述:移动端流量的一大特点是"从社交平台进入"——微信分享可能占了 M 站 30% 以上的流量来源。这套机制确保每一次分享都能生成精美的分享卡片、每一个用户的来源都能被准确追踪、每一次打开都有机会引导用户下载 App。这是一个完整的流量获取 → 用户体验 → 转化引导闭环。


十、改进建议

技术债务

项目中仍存在一些技术债务需要关注:jQuery 依赖未完全移除、无自动化测试覆盖、JCE 类型文件依赖手动生成、ESLint 配置缺失。建议在后续迭代中逐步偿还。

10.1 架构改进

方面现状建议
SSR 框架自研 @hnf-next/light考虑迁移至 Next.js,社区生态更丰富,维护成本更低
状态管理Redux + Thunk对于 SSR 场景,可考虑 React Query / SWR 做数据获取缓存
样式方案全局 SCSS引入 CSS Modules 或 CSS-in-JS,避免样式冲突
Monorepo两个独立项目通用代码(utils、types、TAF 封装)可提取到共享包

10.2 工程化改进

方面现状建议
测试无自动化测试补充 Jest 单元测试(至少覆盖 utils 和 services)
构建工具Webpack 5视频站 VOD 场景可考虑 Vite 提升开发体验
类型安全JCE 手动生成建立 JCE → TypeScript 自动生成流水线
CI/CD手动 deploy 命令集成 GitLab CI,PR 合并自动触发构建部署
Lint未见 ESLint 配置配置 ESLint + Prettier + Husky,统一代码风格

10.3 性能改进

方面现状建议
Hydration全量 Hydration考虑 React 18 Selective Hydration 或 Islands 架构
数据缓存无 SSR 缓存对热门页面的 TAF 响应做 Redis 缓存,降低后端压力
Bundle 体积jQuery 仍在使用移除 jQuery 依赖,用原生 API 替代
图片格式JPG/PNG支持 WebP/AVIF 格式,进一步压缩图片体积

十一、面试常见问题 & 参考回答思路

Q1: 为什么选择 SSR 而不是纯 CSR?

回答思路:

  • SEO 需求:视频站和直播间页面需要被搜索引擎收录,CSR 的 SPA 页面搜索引擎爬虫难以抓取动态内容
  • 首屏性能:SSR 直出 HTML,用户无需等待 JS 下载执行即可看到内容,FCP(First Contentful Paint)显著提升
  • 社交分享:微信等社交平台抓取页面 Meta 信息时需要服务端直出
  • 替代方案对比:预渲染(Prerender)适合静态页面但不适合动态直播数据;SSG 无法处理实时数据
  • 权衡取舍:SSR 增加了服务端计算成本和架构复杂度,通过 LEAF Serverless 平台降低运维负担

Q2: 从 PHP 迁移到 React SSR,如何保证平滑过渡?

回答思路:

  • 渐进式迁移:不是一次性全部重构,而是逐个页面迁移
  • 流量切换:通过 Nginx 配置,逐步将流量从 PHP 切到 Node SSR,出现问题可秒级回滚
  • 兼容处理:新旧页面 URL 保持一致,Cookie/Session 兼容
  • 灰度策略:先内部测试 → 1% 灰度 → 10% → 全量

Q3: TAF/TARS RPC 和普通 HTTP API 有什么区别?为什么用 RPC?

回答思路:

  • 性能:RPC 使用二进制协议(JCE 序列化),比 JSON over HTTP 更紧凑高效
  • 类型安全:通过 JCE IDL 定义接口,编译生成强类型代码,减少运行时错误
  • 服务发现:TAF 注册中心自动管理服务节点,支持负载均衡和故障转移
  • 公司基建:虎牙后端统一使用 TARS 微服务框架,Node.js SSR 层需要对接已有服务
  • 错误处理:封装了超时(3000ms)、降级(返回空响应)机制,保证页面可用性

Q4: 你们的 SSR 框架 @hnf-next/light 和 Next.js 有什么异同?

回答思路:

  • 相同点:文件路由、getServerProps 数据获取、动态路由 [param] 语法
  • 不同点
    • light 是面向 LEAF Serverless 平台设计的,深度集成内部部署体系
    • 支持 rewriteResponse 响应拦截,用于 UA 检测重定向
    • 支持 setHeader / setBodyTag 灵活注入 HTML 内容
    • 不支持 Next.js 的 ISR(增量静态生成)、API Routes 等特性
  • 选择原因:当时 Next.js 对内部 LEAF 平台兼容性不够,需要定制化 SSR 框架

Q5: 如何保证直播间页面的首屏性能?

回答思路:

  • SSR 直出:直播间基础信息(主播信息、房间状态、推荐列表)由服务端直出
  • 异步加载:播放器、弹幕、聊天等重交互模块在客户端 Hydration 后异步初始化
  • 并行请求:getServerProps 中 Promise.all 并行获取 4-5 个接口数据
  • 资源分包:Vendor 框架代码长期缓存,页面级代码按需加载
  • 性能监控:全链路打点(白屏时间、首屏时间、播放器加载时间),数据驱动优化
  • 具体指标:对比 PHP 渲染,首屏时间从 X 秒优化到 Y 秒(需要补充具体数据)

Q6: 弹幕系统是如何设计的?如何处理高并发消息?

回答思路:

  • 消息接收:通过 WebSocket(TAF 信令 SDK)接收实时弹幕消息
  • 消息缓冲:等待队列最大 200 条,超出丢弃旧消息(保证最新消息优先展示)
  • DOM 性能:采用对象池模式复用 DOM 节点,避免频繁创建销毁
  • 动画方案:CSS Transform translateX 动画,利用 GPU 加速
  • 轨道分配:10 条弹幕轨道,动态检测空闲轨道分配
  • 流控策略:根据消息积压量动态调整弹幕滚动速度

Q7: 如何做错误监控和性能优化?

回答思路:

  • 错误监控:Sentry 全量采集(sampleRate: 1),记录用户 ID、URL、错误堆栈
  • 性能采集:Performance API 提取 DNS/TCP/TTFB/DOM 等关键指标
  • 播放质量:黑屏检测(10s 超时上报)、卡顿计数(20s 周期上报)
  • 数据上报:YA Report 事件系统,支持 PV、点击、自定义事件
  • 闭环优化:监控数据 → 发现瓶颈 → 针对性优化 → 验证效果

Q8: Webpack 构建优化做了哪些工作?

回答思路:

  • 代码分割:框架代码(React/Redux)独立 vendor chunk,长期缓存
  • 哈希命名[chunkhash] 实现精确缓存失效
  • 资源优化:图片哈希重命名 + CDN 分发
  • 构建缓存:Hard Source Plugin + Cache Loader 加速二次构建
  • Manifest 映射:自动生成路由→资源映射,SSR 精确注入
  • Tree Shaking:Webpack 5 原生支持,Babel 配置 modules: false
  • 外部化服务端代码externals 排除 server 代码进入客户端 bundle

Q9: 如何处理 M 站和 PC 站的跨端跳转?

回答思路:

  • 服务端检测:在 rewriteResponse 中通过 User-Agent 检测设备类型
  • M 站:桌面 UA 访问 → 302 跳转到 www.huya.com 对应页面
  • 视频站:移动 UA 访问 → 302 跳转到 m.v.huya.com 对应页面
  • 客户端兜底:HTML 中注入 redirect 脚本,JS 层二次检测
  • 参数透传:跳转时保留 platFormfrom 等追踪参数

Q10: 项目中的安全防护措施有哪些?

回答思路:

  • XSS 防护:所有用户输入通过 filterXss() 过滤后再渲染到 HTML
  • Cookie 安全:TAF 请求头中传递 Cookie 用于服务端鉴权,不在前端暴露敏感信息
  • CORS 处理:跨域请求通过 JSONP Adapter 或服务端代理
  • Origin Trial:SharedArrayBuffer 通过正规的 Origin Trial 机制启用
  • 输入校验:视频 ID 严格校验 (/^\d+\.html$/),防止注入攻击

十二、项目数据与成果

以下数据来源于职级评审材料,供面试参考:

  • 重构范围:M 站全站 + 视频站全站,覆盖所有核心页面
  • 技术升级:PHP Smarty → React 17 + TypeScript + SSR
  • 部署方式:传统服务器 → LEAF Serverless,运维成本大幅降低
  • 首屏性能:SSR 直出显著优于 PHP 模板渲染(具体数据见内部性能报表)
  • 开发效率:组件化开发,新页面开发周期缩短;TypeScript 减少线上 Bug 率
  • SEO 效果:SSR 保证搜索引擎完整抓取页面内容,收录量无下降

附录:关键文件速查表

文件路径说明
pages/*.tsx页面入口,定义路由和 SSR 数据获取
common/server/sevices/TAF RPC 服务调用层
common/server/utils/function.tstafRequest 通用封装
common/utils/function.ts (或 common/utils.ts)通用工具函数
common/utils/constant.ts (或 common/constant.ts)环境常量和 HTML 注入
common/store/Redux Store 定义(M 站)
common/client/modules/客户端交互模块
common/client/utils/report.ts数据上报初始化
common/client/utils/sentry.ts错误监控初始化(视频站)
build/config.ts构建路径和环境配置
build/webpack/webpack.*.tsWebpack 配置(base/dev/build)
jce/JCE 协议文件(RPC 接口定义)
serverless/Serverless 函数(M 站)
api/healthCheck.ts健康检查接口
manifest.json构建产物的路由→资源映射