面经原文内容
📍面试公司:字节跳动
🕐面试时间:3 月 3 日
💻面试岗位:音视频前端(春招)
❓面试问题:
- 自我介绍
- 用了哪些方法使 FCP 渲染耗时缩短近 1s
- 有没有遇到过哪个包不支持 ESM 的,你是怎么处理的
- 你进行代码分割的思路是什么
- 不定高的虚拟列表的实现原理是什么
- NextJS 有哪些渲染模式,分别介绍一下
- 你们的应用用的是哪种
- NextJS 中有一个内置的图片优化组件,它做了哪些优化
- 让你优化图片显示你怎么优化
- webp 格式的图片有兼容性问题吗,怎么做降级处理
📝 字节跳动音视频前端一面·深度解析
🎯 面试整体画像
| 维度 | 特征 |
|---|---|
| 公司定位 | 字节跳动 - 音视频方向 |
| 面试风格 | 项目深潜型 + 场景追问型 |
| 难度评级 | ⭐⭐⭐⭐(四星) |
| 考察重心 | 性能优化、工程化、Next.js、图片处理 |
💡 面经关键点解读
面试官的潜台词:音视频业务对性能要求极高,所以 FCP 优化要问到具体数值;项目中遇到过的工程化问题能看出你的实战深度;虚拟列表是长列表场景的必备技能;Next.js 是字节很多中台项目的首选框架;图片优化直接影响用户体验。这些问题环环相扣,考察的是一个前端工程师的全链路优化能力。
🔍 逐题深度解析
一、FCP 优化
问题:用了哪些方法使 FCP 渲染耗时缩短近 1s
FCP(First Contentful Paint)是衡量首屏加载体验的核心指标。
// 1. 关键 CSS 内联
// 将首屏所需 CSS 直接内联到 HTML,避免网络请求
<style>
.header {height: 60px; background: #fff;}
.hero {height: 400px; background:url('hero-small.jpg');}
/* 只包含首屏样式,约 2KB */
</style>
// 2. 预加载关键资源
// 告诉浏览器优先下载关键资源
<link rel="preload" href="critical.js" as="script">
<link rel="preload" href="hero.webp" as="image">
<link rel="preconnect" href="https://fonts.googleapis.com">
// 3. 图片优化
// 使用 WebP 格式 + 响应式图片
<picture>
<source srcset="hero-320.webp 320w, hero-640.webp 640w" type="image/webp">
<img src="hero-640.jpg" srcset="hero-320.jpg 320w, hero-640.jpg 640w" loading="eager">
</picture>
// 4. 字体优化
@font-face {
font-family:'CustomFont';
src:url('font.woff2') format('woff2');
font-display: swap;
// 文本立即用降级字体显示,加载后替换
}
// 5. 骨架屏
<div class="skeleton">
<div class="skeleton-header"></div>
<div class="skeleton-content"></div>
</div>
// 6. 移除阻塞渲染的 JS
<script src="analytics.js" async defer></script>
// 7. 服务端渲染/静态生成
// Next.js 的 SSG/SSR 直接返回 HTML
二、ESM 兼容性问题
问题:有没有遇到过哪个包不支持 ESM 的,你是怎么处理的
在工程化实践中,经常会遇到一些老包只提供 CommonJS 版本,导致 Tree Shaking 失效或 Next.js 报错。
// 问题示例:some-old-package 只提供 CommonJS
const{ debounce }=require('some-old-package')
// ❌
// 解决方案 1:next-transpile-modules(Next.js 专用)
// next.config.js
const withTM =require('next-transpile-modules')(['some-old-package','another-cjs-pkg'])
module.exports =withTM({/* 其他配置 */})
// 解决方案 2:webpack 配置(通用)
module.exports ={
webpack:(config)=>{
config.module.rules.push({
test:/\.js$/,
include:/node_modules\/(some-old-package|another-pkg)/,
use:{
loader:'babel-loader',
options:{
presets:['@babel/preset-env'],
plugins:['@babel/plugin-transform-modules-commonjs']
}
}
})
return config
}
}
// 解决方案 3:动态导入(运行时降级)
const somePackage =await import('some-old-package').then(m=> m.default || m)
// 解决方案 4:patch-package 直接修改
// 创建补丁文件 patches/some-old-package+1.2.3.patch
// 手动修改导出方式为 ESM
实际案例:曾遇到 marked 库某个版本 ESM 导出有问题,最终锁定到兼容版本并配置 transpile 解决。
三、代码分割思路
问题:你进行代码分割的思路是什么
代码分割的核心是'按需加载',让用户只下载当前页面所需的代码。
// 1. 路由级别分割(最常用)
// React
const Home =lazy(()=>import('./pages/Home'))
const About =lazy(()=>import('./pages/About'))
// Vue
const routes =[
{path:'/',component:()=>import('@/pages/Home.vue')}
]
// 2. 组件级别分割
const Editor =dynamic(()=>import('@/components/Editor'),{
loading:()=><Loading />,
ssr:false // 不需要服务端渲染的组件
})
// 3. 三方库分割(webpack 配置)
splitChunks:{
chunks:'all',
cacheGroups:{
vendor:{
test:/[\\/]node_modules[\\/]/,
name:'vendors',
priority:10
},
ui:{
test:/[\\/]node_modules[\\/]/(antd|element-ui)[\\/]/,
name:'ui-libs',
priority:20
}
}
}
(()){
().({
.()
})
}
<link rel= href=>
observer.(linkElement)
分割思路总结:先路由后组件,三方库单独打包,动态功能按需加载,空闲时间预加载。
四、不定高虚拟列表
问题:不定高的虚拟列表的实现原理是什么
虚拟列表的核心是只渲染可视区域内的元素。不定高比定高复杂,因为无法预知每个元素的高度。
// 核心数据结构:维护每个元素的位置信息
class VirtualList{
constructor(){
this.positions =[] // [{ top, bottom, height }]
this.totalHeight =0
}
// 1. 初始化:使用预估高度
initPositions(items, estimatedHeight =100){
let top =0
this.positions = items.map((_, index)=>{
const height = estimatedHeight
const position ={ index, height, top,
bottom: top + height
}
top += height
return position
})
this.totalHeight = top
}
// 2. 动态更新:渲染后测量真实高度
updateHeight(index, realHeight){
const pos =this.positions[index]
const diff = realHeight - pos.height
if(diff !==0){
pos.height = realHeight
pos.bottom += diff // 更新后续所有元素的位置
for(let i = index +1; i <this.positions.length; i++){
this.positions[i].top += diff
this.[i]. += diff
}
. += diff
}
}
(){
left =
right =.. -
(left <= right){
mid = .((left + right)/)
pos =.[mid]
(pos. < scrollTop){ left = mid +}
(pos. > scrollTop){ right = mid -}
{ mid }
}
left
}
(){
start =.(scrollTop)
end =.(scrollTop + containerHeight)
returnthis..(start, end +).(({...pos,
}
}
(items, itemHeight =){
[positions, setPositions]=([])
({
top =
newPositions = items.({
pos ={ top,: itemHeight,: top + itemHeight }
top += itemHeight
pos
})
(newPositions)
},[items])
constupdateItemHeight={
({
newPositions =[...prev]
diff = height - newPositions[index].
newPositions[index]. = height
newPositions[index]. += diff
( i = index +; i < newPositions.; i++){
newPositions[i]. += diff
newPositions[i]. += diff
}
newPositions
})
}
{ positions, updateItemHeight }
}
五、Next.js 渲染模式
问题:NextJS 有哪些渲染模式,分别介绍一下
Next.js 提供了多种渲染模式,适应不同场景需求:
| 模式 | 原理 | 特点 | 适用场景 |
|---|---|---|---|
| SSR | 每次请求时在服务器渲染 | 实时性好、SEO 友好 | 个性化页面、需实时数据 |
| SSG | 构建时生成静态 HTML | 极快、可 CDN 缓存 | 博客、文档、营销页 |
| ISR | 静态生成 + 定期更新 | 兼顾速度与实时性 | 产品页、新闻站 |
| CSR | 客户端渲染 | 强交互 | 后台管理、个人中心 |
// SSR - Server-Side Rendering
exportasyncfunctiongetServerSideProps(context){// 每次请求执行
const data =awaitfetch(`/api/user/${context.params.id}`)
return{props:{ data }}
}
// SSG - Static Site Generation
exportasyncfunctiongetStaticProps(){// 构建时执行
const posts =awaitfetchPosts()
return{props:{ posts }}
}
// ISR - Incremental Static Regeneration
exportasyncfunctiongetStaticProps(){
const data =awaitfetchData()
return{props:{ data },revalidate:60// 每 60 秒重新生成}
}
// 动态路由的静态生成
exportasyncfunctiongetStaticPaths(){
const posts =awaitfetchPostIds()
const paths = posts.map(p=>({params:{id: p.id }}))
return{ paths,fallback:'blocking'}// fallback 处理未生成的路径
}
// CSR - Client-Side Rendering
functionPage(){
const[data, setData]=useState(null)
useEffect(()=>{
fetchData().(setData)
},[])
}
六、应用渲染模式选择
问题:你们的应用用的是哪种
根据业务场景混合使用:
// 1. 官网/营销页:SSG
// 原因:内容相对固定,需要极致速度和 SEO
exportgetStaticProps(){...}
// 2. 博客/文档:SSG + ISR
// 原因:内容更新不频繁,但需要实时发布
exportgetStaticProps({revalidate:3600}){...}
// 3. 用户个人中心:CSR
// 原因:强交互,需要登录态
// 直接客户端渲染
// 4. 详情页:SSR 或 ISR
// 如果内容动态变化快:SSR
// 如果内容变化慢:ISR
exportgetServerSideProps(){...}
// 5. 混合示例
// pages/index.js - SSG
exportgetStaticProps(){...}
// pages/product/[id].js - ISR
exportgetStaticPaths(){...}
exportgetStaticProps({revalidate:60}){...}
// pages/user/profile.js - CSR
// 不导出任一函数,默认 CSR
选择标准:
- SEO 要求高 → 用 SSG/SSR
- 数据实时性要求高 → 用 SSR
- 速度要求高、数据变化慢 → 用 SSG
- 需要登录态 → 用 CSR
- 数据变化快但能接受延迟 → 用 ISR
七、Next.js 图片优化组件
问题:NextJS 中有一个内置的图片优化组件,它做了哪些优化
import Image from'next/image'
<Image src="/hero.jpg" alt="Hero" width={1200} height={600} priority quality={75} placeholder="blur" blurDataURL="data:image/jpeg;base64,/9j/..."/>
// 1. 响应式图片
// 自动生成多尺寸,设置 srcset
// 输出:<img srcset="/_next/image?w=640 640w,/_next/image?w=750 750w,/_next/image?w=828 828w" />
// 2. 格式优化
// 根据浏览器支持自动选择 WebP/AVIF
// Chrome 收到 WebP,Safari 收到 JPEG
// 3. 懒加载
// 默认启用 IntersectionObserver
// 进入视口才加载
// 4. 预加载关键图片
// priority 属性会添加到预加载清单
<link rel="preload"as="image" href="/hero.jpg">
// 5. 防止布局偏移
// 必须指定宽高,自动计算 aspect-ratio
<div style="aspect-ratio: 1200/600">
<img ...>
</div>
// 6. 占位图 placeholder="blur"
// 显示模糊占位 blurDataURL // 自定义 base64 占位图
// 7. 图片域名配置
// next.config.js
module.exports ={
images:{
domains:['example.com','cdn.example.com'],
formats:['image/webp','image/avif']
}
}
// 8. 优化效果
// 体积:JPEG 200KB → WebP 60KB(-70%)
// 加载:原本需要手动处理的工作自动完成
八、图片显示优化
问题:让你优化图片显示你怎么优化
// 1. 格式选择
// WebP > AVIF > JPEG/PNG
// WebP 比 JPEG 小 25-35%,比 PNG 小 80%
// 2. 响应式图片
<img src="image-800.jpg" srcset=" image-400.jpg 400w, image-800.jpg 800w, image-1200.jpg 1200w " sizes=" (max-width: 500px) 400px,(max-width: 900px) 800px, 1200px " >
// 3. 懒加载
<img loading="lazy" src="image.jpg">
// 4. 渐进式加载
functionProgressiveImage({ src, placeholder }){
const[currentSrc, setCurrentSrc]=useState(placeholder)
useEffect(()=>{
const img =newImage()
img.src = src
img.onload=()=>setCurrentSrc(src)
},[src])
return(
<img src={currentSrc} style={{filter: currentSrc === placeholder ?'blur(10px)':'none'}}/>
)
}
// 5. 使用 CDN
// 加上图片处理参数 https://cdn.example.com/image.jpg?w=800&q=75&format=webp
// 6. 缓存策略
// 强缓存 + hash 文件名 Cache-Control: max-age=31536000
// image-8f3c9d.jpg
// 7. 预加载关键图片
<link rel="preload"as="image" href="hero.jpg">
// 8. 使用 Intersection Observer
const observer =newIntersectionObserver((entries)=>{
entries.forEach(entry=>{
if(entry.isIntersecting){
const img = entry.target
img.src = img.dataset.src
observer.unobserve(img)
}
})
})
九、WebP 兼容性降级
问题:webp 格式的图片有兼容性问题吗,怎么做降级处理
// 1. picture 标签(最推荐)
<picture>
<source srcset="image.webp" type="image/webp">
<source srcset="image.jp2" type="image/jp2">
<img src="image.jpg" alt="fallback">
</picture>
// 2. 特性检测
functionsupportsWebP(){
const canvas = document.createElement('canvas')
if(canvas.toDataURL){
return canvas.toDataURL('image/webp').indexOf('image/webp')===0
}
returnfalse
}
// 使用
const src =supportsWebP()?'image.webp':'image.jpg'
// 3. 服务端检测
app.get('/image',(req, res)=>{
const accept = req.headers['accept']
if(accept && accept.includes('image/webp')){
res.setHeader('Content-Type','image/webp')
res.sendFile('image.webp')
}else{
res.sendFile('image.jpg')
}
})
// 4. Modernizr
if(Modernizr.webp){
// 使用 WebP
}else{
// 使用降级格式
}
// 5. Next.js 自动处理
<Image src="/image.jpg"/>
// 自动检测 Accept 头,返回对应格式
// 6. 批量转换工具
// 构建时生成 webp 版本
const imagemin =require('imagemin')
const imageminWebp =require('imagemin-webp')
awaitimagemin(['images/*.{jpg,png}'],{
destination:'public/images',
plugins:[imageminWebp({quality:75})]
})
// 7. 动态加载
const webpSupported =awaitcheckWebPSupport()
const imageUrl = webpSupported ?'/img.webp':'/img.jpg'
📚 知识点速查表
| 知识点 | 核心要点 |
|---|---|
| FCP 优化 | 关键 CSS、预加载、图片优化、字体优化、骨架屏 |
| ESM 兼容 | transpile、webpack 配置、动态导入、patch-package |
| 代码分割 | 路由级、组件级、三方库、动态导入、预加载 |
| 不定高虚拟列表 | 预估高度、动态更新、二分查找、位置数组 |
| Next 渲染模式 | SSR(实时)、SSG(静态)、ISR(增量)、CSR(客户端) |
| 图片优化组件 | 响应式、格式转换、懒加载、预加载、布局稳定 |
| 图片优化 | 格式选择、响应式、懒加载、CDN、缓存 |
| WebP 降级 | picture 标签、特性检测、服务端判断、Modernizr |


