← 返回全部分类

前端开发面试题(27道)

前端开发 · 中等

"请讲一下CSS的Flex布局和Grid布局的区别和使用场景。"

Flex和Grid是CSS现代布局的两大利器,核心区别在于维度。Flex是一维布局系统,一次只能处理一个方向(行或列),适合组件内部的元素排列。主轴方向通过flex-direction控制,主轴对齐用justify-content(center、space-between、space-around等),交叉轴对齐用align-items。flex属性是flex-grow、flex-shrink、flex-basis的简写,控制元素如何分配剩余空间。flex-wrap开启后元素可以换行,但换行后每行独立计算,无法对齐列——这就是Flex的一维局限。Grid是真正的二维布局,可以同时定义行和列。grid-template-columns: repeat(3, 1fr)创建三等分列,minmax(200px, 1fr)实现响应式最小宽度。grid-template-areas允许用命名区域进行语义化布局,非常直观。fr单位是Grid特有的,按比例分配剩余空间。使用场景上,导航栏、工具栏、表单一行内的对齐用Flex;整体页面布局(header/sidebar/main/footer)、卡片网格、仪表盘用Grid。实际项目中两者结合使用效果最好:外层用Grid搭建页面骨架,Grid单元格内部用Flex排列元素。响应式设计方面,Grid的auto-fill配合minmax()可以不用媒体查询就实现自适应列数:grid-template-columns: repeat(auto-fill, minmax(250px, 1fr))。
💡 提示:面试时可能要求现场写代码实现水平垂直居中——Flex用justify-content+align-items,Grid用place-items:center。;掌握几个经典布局模式:圣杯布局、双飞翼布局用Grid如何实现。;强调两者是互补关系而非替代关系,展示你的工程思维。;现场写CSS时如果忘了属性名,即答侠可以实时提供正确的属性和值。
前端开发 · 中等

"请讲一下React Hooks的原理和使用要点。"

React Hooks的底层实现基于链表结构。每个组件实例维护一个hooks链表,组件每次渲染时按顺序执行各个hook,通过索引匹配对应的状态。这就是为什么hooks必须在顶层调用、不能放在条件语句中——一旦顺序变了,状态就会错乱。useState返回状态值和更新函数,调用setter后React会将更新加入队列并批量处理(React 18默认所有更新都自动批处理)。useEffect在DOM渲染后异步执行,依赖数组控制执行时机:空数组相当于componentDidMount,不传则每次渲染都执行。返回的清理函数在下次effect执行前和组件卸载时调用。最常见的陷阱是闭包问题:useEffect内部捕获的是当次渲染时的state值,不是最新值。解决方案包括把依赖加入依赖数组、使用useRef保存最新值、或使用setState的函数形式。useRef创建的对象在组件整个生命周期内保持同一引用,修改.current不会触发重渲染,适合存储定时器ID、DOM引用、前一次渲染的值等。性能优化方面,useMemo缓存计算结果,useCallback缓存函数引用,两者都接受依赖数组。但不应该滥用——每个memo都有比较依赖的开销,只在确实有性能问题时才使用。自定义Hook是Hooks最强大的特性,比如useDebounce、useFetch、useLocalStorage等,通过组合基础Hooks实现复杂逻辑,并可跨组件复用。React 18新增了useTransition用于标记非紧急更新,useDeferredValue延迟处理低优先级渲染。
💡 提示:useEffect的依赖数组是面试重灾区——一定要理解缺少依赖导致的闭包陷阱和无限循环。;不要对所有函数和值都使用useMemo/useCallback——过度优化反而降低性能和可读性。;准备一个自定义Hook的实际例子(如useDebounce),展示你的实战经验。;面试时如果忘了某个Hook的参数或行为,即答侠可以实时提供准确的API信息。
前端开发 · 中等

"请讲一下从输入URL到页面渲染的完整过程。"

输入URL后,浏览器首先进行DNS解析获取IP地址(先查浏览器缓存→系统缓存→路由器缓存→ISP DNS→递归查询),然后建立TCP连接(三次握手),HTTPS还需TLS握手协商密钥。服务器返回HTML后,浏览器开始增量解析构建DOM树。遇到<link>标签时异步加载CSS并构建CSSOM——CSS不阻塞DOM解析但阻塞渲染。遇到<script>标签(无async/defer)时会停止DOM解析,等JS下载并执行完毕后继续,因为JS可能修改DOM结构。所以推荐将脚本放在body底部或使用defer属性。DOM树和CSSOM都构建完成后,浏览器将两者合并生成渲染树——display:none的元素不会进入渲染树,但visibility:hidden会(占据空间但不可见)。接下来是布局阶段(Layout/Reflow),从根节点开始递归计算每个元素的精确几何信息:位置、宽度、高度。任何改变元素尺寸或位置的操作都会触发回流,回流会沿DOM树向上传播影响父元素和兄弟元素,开销很大。然后是绘制阶段(Paint),将布局信息转化为实际像素——颜色、边框、阴影、文字。最后是合成阶段(Composite),浏览器将页面分成多个图层,使用GPU进行合成。带有transform、opacity、will-change的元素会被提升为独立合成层,修改这些属性只需重新合成,跳过布局和绘制,所以CSS动画用transform比用top/left性能好得多。性能优化方面:减少关键渲染路径的资源数量、批量读写DOM(避免强制同步布局)、使用requestAnimationFrame调度视觉更新、图片懒加载、CSS containment限制回流范围。
💡 提示:回答要有层次感,网络→解析→渲染→优化,逐层深入,面试官会根据你的回答深度来追问。;一定要区分回流(Reflow)和重绘(Repaint),这是最常见的追问点。;提到合成层和GPU加速是加分项,展示你对高性能动画的理解。;面试中如果某个阶段细节记不清,即答侠可以实时提供关键流程和术语。
前端开发 · 中等

"请讲一下JavaScript的事件循环机制。"

JavaScript是单线程语言,只有一个主线程和一个调用栈。事件循环(Event Loop)是实现异步非阻塞的关键机制。执行过程如下:首先执行全局同步代码,遇到异步任务(如setTimeout、fetch)时将其交给浏览器的Web API处理,完成后将回调推入对应的任务队列。任务队列分两种:微任务队列(Microtask Queue)包含Promise.then/catch/finally、MutationObserver、queueMicrotask;宏任务队列(Macrotask Queue)包含setTimeout、setInterval、setImmediate(Node)、I/O回调、UI渲染事件。事件循环的核心规则是:调用栈清空后,先把微任务队列中的所有任务依次执行完(如果执行微任务过程中又产生了新的微任务,也在本轮一并执行),然后才从宏任务队列中取一个任务执行。执行完一个宏任务后,又会先清空微任务队列,如此循环。举例:console.log(1); setTimeout(()=>console.log(2), 0); Promise.resolve().then(()=>console.log(3)); console.log(4); 输出顺序是1、4、3、2。因为1和4是同步代码先执行,Promise.then是微任务优先于setTimeout宏任务执行。async/await方面,async函数中await之前的代码同步执行,await后面的表达式会立即求值,但await之后的代码相当于放入.then()回调,变成微任务。注意Promise构造函数中的代码是同步执行的,只有.then里的回调才是微任务。在Node.js中还有process.nextTick,优先级比Promise.then更高,属于微任务但在微任务队列最前面执行。
💡 提示:准备好手写代码输出题——这是事件循环最常见的考察方式,面试官会给你一段混合sync/Promise/setTimeout的代码让你说输出顺序。;记住:Promise构造函数是同步执行的,.then才是微任务,很多人搞混。;async函数中await前的代码是同步的,这个细节经常被忽略。;面试中如果遇到复杂的执行顺序题,即答侠可以帮你实时分析输出。
前端开发 · 中等

"HTTP和HTTPS的区别是什么?TLS握手过程讲一下。"

HTTP是明文,网络上任何人都能看和改。HTTPS把HTTP包在TLS里,提供三个保证:加密(机密性)、MAC(完整性)、证书(服务端身份)。TLS 1.2握手逻辑上四步。客户端发ClientHello:携带支持的cipher suites列表和client random。服务端发ServerHello:选一个cipher、发server random、发证书。客户端校验证书链——服务端证书被中间CA签,中间CA证书被根CA签,根CA是操作系统或浏览器预置信任的。校验从叶子一路向上走到受信根。校验通过后,客户端生成premaster secret,用证书里的公钥加密发给服务端。双方现在都有client random、server random、premaster secret三个值,通过KDF派生出对称会话密钥。之后的应用数据都用AES-GCM或ChaCha20对称加密。为什么非对称后转对称?非对称加密比对称慢百倍以上,只用来交换一个key;实际数据量大,必须用对称加密。TLS 1.3显著改善了握手。把握手压到1-RTT——客户端在ClientHello里就推测性地发密钥材料;支持0-RTT会话恢复(但有重放风险的取舍)。删掉了所有弱密码套件——RC4、MD5、CBC padding oracle这些全没了。ECDHE强制,所以每个会话都有前向安全(PFS)——就算服务端私钥未来被泄露,之前抓到的流量也无法解密。所有模式都是AEAD,认证和加密绑定在一起。
💡 提示:一定要讲"非对称交换key+对称传数据"的组合逻辑。;讲到ECDHE和前向安全(PFS),是中高级岗期望的答案。;TLS 1.3的改进是高价值追问:1-RTT、0-RTT、强制AEAD。;忘了ECDHE、AEAD这种术语,即答侠可以实时提示。
前端开发 · 中等

"Vue 3的响应式是怎么实现的?为什么从defineProperty换成Proxy?"

Vue 2用Object.defineProperty做响应式。组件创建时,Vue遍历data的每个属性,替换成getter/setter,读时dep.depend()、写时dep.notify()。这套有具体问题。第一,检测不到属性增删——this.user.age = 20在age原本不存在时完全不触发更新。第二,拦不住数组下标和.length——arr[0] = x、arr.length = 0都不响应,所以Vue 2给7个数组方法打补丁(push、pop、splice等)手动触发。第三,创建时要递归整个对象树,大对象启动非常慢。Vue 3用Proxy。Proxy是原生JS特性,用一个handler把对象包起来,拦截所有操作——get、set、has、deleteProperty、ownKeys全覆盖。不需要预处理。get陷阱里调track(target, key)记录"当前活跃的effect依赖这个属性";set陷阱里调trigger(target, key)通知所有跟踪过这个属性的effect。属性增删、数组下标、Map/Set变更全部天然捕获。Proxy还是惰性的——顶层对象被包,访问到某个嵌套属性时Vue才把嵌套值再包一层Proxy返回。也就是说只有真正触达的路径才会有响应式开销,大state树启动成本骤降。两个主要API:reactive(obj)给对象返回Proxy。ref(value)处理原始值——原始值不能被Proxy包,所以ref返回{value: x}外面套响应式,访问.value会被拦截。模板里ref会自动解包,所以模板里写refName而不是refName.value,但script代码里必须写.value,这是最常见的踩坑点。track和trigger是底层原语,computed、watch、render effect本质上都是effect()——在读时注册依赖,写时重新运行。
💡 提示:一定要和Vue 2对比着讲,这是面试官真正想听的。;讲到惰性深响应是读过源码的信号。;能解释"原始值不能被Proxy所以ref要包.value"是常见追问。;临场忘了track/trigger,即答侠可以实时提示。
前端开发 · 中等

"讲一下CORS。什么情况会触发预检请求?相关头有哪些?"

CORS(跨域资源共享)是浏览器允许脚本发跨域请求的机制。基础的同源策略规定:脚本默认只能读取同源(协议+域名+端口三者相同)的响应。这是为了防止evil.com的脚本调fetch("https://bank.com/api/balance")读你已登录的银行账户。CORS通过服务端响应头opt-in放开这个限制。跨域请求分两种。简单请求:GET或HEAD或POST,且Content-Type在安全白名单(application/x-www-form-urlencoded、multipart/form-data、text/plain),且没有自定义头比如Authorization。浏览器直接发带Origin头的请求。服务端可以响应Access-Control-Allow-Origin: https://a.com(或*)。响应头匹配浏览器才允许JS读响应;不匹配就拦截——注意请求还是到了服务端,响应只是JS读不到。预检请求:其他情况都要预检。包括PUT、DELETE、PATCH;或Content-Type: application/json;或带Authorization等自定义头。浏览器先发OPTIONS,带Access-Control-Request-Method: PUT和Access-Control-Request-Headers: Authorization, Content-Type。服务端必须返回Access-Control-Allow-Methods、Access-Control-Allow-Headers、Access-Control-Allow-Origin全部匹配。通过后真请求才发。预检可以缓存Access-Control-Max-Age秒内的相同请求跳过预检。凭证(cookie、HTTP Basic auth、TLS客户端证书)默认不跨域发送。要带cookie:客户端fetch的credentials设成"include"(或XHR的withCredentials=true)+服务端响应Access-Control-Allow-Credentials: true + Access-Control-Allow-Origin必须是具体源(通配符*被拒绝)。这三个条件都满足才行,是生产最常见的CORS坑。最后:CORS是浏览器强制的,Node或后端服务发同样的跨域HTTP请求没有任何CORS限制,因为没有浏览器参与。
💡 提示:先讲CORS的安全动机再讲机制,面试加分。;简单请求 vs 预检请求的三个判定条件要记牢。;"带cookie时Access-Control-Allow-Origin不能是*"是面试金句,立即显示实战经验。;临场忘了响应头名字,即答侠可以实时提示Access-Control-Allow-Origin/Methods/Headers。
前端开发 · 中等

"什么时候用TypeScript比JavaScript更合适?代价是什么?"

TypeScript是加了静态类型系统和编译期检查的JavaScript。编译产物就是普通JS,没有运行时开销、没有语义变化。价值完全在开发工具和提前发现bug上。TS给你三个JS没有的东西。第一,IDE真正懂你的代码——自动补全知道API形状、属性typo实时报错、跨模块跳转定义。第二,重构自信——重命名一个属性,大仓里所有引用都更新,不用"希望grep全找到了"。第三,避免线上bug——尤其是null/undefined访问、参数顺序错、漏传必填字段。TS用结构化类型——值和类型是否匹配看形状不看名字。函数参数类型{name: string, age: number}接受任何字面量对象只要字段对。比Java或C#的名义类型灵活,很契合JS处理JSON的主要场景。真正的问题是什么时候值得用。3人以上团队共写一份代码持续6个月以上,重构和文档价值指数级累积——类型注解就是永不过时的机器校验文档。API重的代码——后端响应→前端状态→UI props之间反复映射,每层有类型能提前抓到shape不一致,JS环境会直接崩。成本侧:编译步骤增加CI时间、无类型的第三方库要@types或any(不写容易传染)、strict模式下要纪律维持,每个any或as都在悄悄削弱保护。一次性脚本、原型、hackday代码,TS的overhead划不来。100K行的团队项目,TS明显值。我的判断规则:如果这代码会被两个工程师在不同时间读,就用TS。
💡 提示:要承认TS有代价——只列优点显得naive。;能讲"结构化类型"是差异化信号,大部分候选人只说"加了类型"。;"团队规模×持续时间"的判断框架好记且显务实。;临场忘了"结构化类型"这个词,即答侠可以实时提示。
前端开发 · 中等

"Webpack和Vite有什么区别?为什么Vite的dev启动快这么多?"

Webpack和Vite解决同样的问题——把源码变成浏览器能跑的东西——但dev模式走了相反的路。Webpack的dev server要打包。启动时建完整依赖图,每个文件跑一遍所有loader(babel-loader、ts-loader、css-loader等),产出一个bundle,再serve。中等规模应用冷启动10-30秒。每次保存文件,HMR从改动模块沿着图走,重新生成受影响的chunk。Vite走了完全不同的思路。dev模式下Vite根本不打包,直接以原生ES模块形式通过HTTP把源文件发给浏览器。浏览器加载index.html时看到import语句,为每个模块发独立HTTP请求;Vite现场转换每个请求(TS→JS、JSX→JS、CSS→JS import),用的是esbuild,比Babel快一两个数量级。第三方依赖——React、lodash等——用esbuild预打包成每个依赖一个ESM文件,因为node_modules里一个包可能有几百个小文件,让浏览器一个个请求太慢。所以Vite的dev冷启动近乎瞬时——什么都没提前打包。HMR也瞬时——改动只需重新转换单个文件,不走任何图。为什么Webpack不照做?浏览器原生ESM最近才实用化,Webpack的架构是围绕打包设计的。生产环境Vite反而会打包——用Rollup产出传统chunked bundle,因为真实用户访问不能每页几百个HTTP请求。所以Vite是"dev ESM,prod Rollup"两个模式。选型:2026年新项目默认Vite。Webpack继续适合:巨型遗留代码迁移代价超过收益的、复杂自定义loader链或Module Federation等Webpack工具链更深的场景。Rspack是Rust重写的Webpack兼容版,想要Vite速度又不想重写Webpack配置的团队选它。
💡 提示:"Webpack预打包 vs Vite原生ESM按需"的对比是答题核心。;能讲esbuild预打包第三方依赖是Vite快的另一半原因。;指出"Vite prod用Rollup不用ESM"是很多候选人漏掉的细节。;临场忘了esbuild、Rollup这些名字,即答侠可以实时提示。
前端开发 · 中等

React Fiber是什么?为什么要重写协调器?

React 16之前是递归协调——一次长长的同步树遍历。大树就会卡主线程掉帧。Fiber把递归改成了对fiber链表节点的迭代,调度器能在任意节点暂停。render阶段可中断、构建WIP树;commit阶段同步、把DOM变更原子地刷下去。这套架构解锁了Suspense、Transition、并发渲染——没有可中断能力这些都做不到。面试时的关键一句:React要"聪明地"决定先渲染什么,Fiber就是实现这种聪明的发动机。
💡 提示:别把Fiber(架构)和Concurrent Mode(特性)混为一谈。;双缓冲——"current树 vs WIP树"——是核心心智模型。;即答侠会把Fiber和Suspense/Transition例子绑在一起讲——答得具体,不是照本宣科。
前端开发 · 中等

Vue 3相比Vue 2有哪些改进?

对我影响最大的三点:响应式重写(Proxy可以响应新增属性和数组索引赋值,不用$set了)、Composition API(setup+ref/reactive能像React hooks那样抽可复用的composable)、编译器重写(静态提升+patch flag让每次更新派发的代码更少)。TypeScript体验从Vue 2的"能用但难受"变成Vue 3的"一等公民"——script setup+defineProps真香。迁移策略:新文件用Composition API,老Options API别动,两者共存互操作没问题。
💡 提示:Vue 2已于2023年12月EOL——新项目必须Vue 3。;Pinia取代Vuex成为Vue 3官方状态管理。;即答侠特别标了响应式的坑(Vue 2的$set、Proxy对Map的限制)——面试官最爱问边角。
前端开发 · 中等

讲讲事件循环。setTimeout和Promise穿插输出什么?

每轮:一个宏任务跑完,然后整个微任务队列清空,然后(浏览器里)可能渲染。经典题:`console.log(1); setTimeout(()=>console.log(2)); Promise.resolve().then(()=>console.log(3)); console.log(4)` 输出1、4、3、2——整段脚本是一个宏任务,同步先跑,然后微任务(3)清空,再下一个宏任务(setTimeout里的2)。常见坑:函数里的`await x`会把后面代码变成微任务,即使x已resolve,await后面的代码也是在调用方同步代码跑完之后才跑。心智模型:宏任务=粗粒度任务边界,微任务=细粒度的"收尾动作"。
💡 提示:queueMicrotask(fn)直接API——需要微任务顺序又不想造Promise时用。;微任务无限递归会饿死渲染——页面卡住。setTimeout(fn,0)能让出一帧。;即答侠把这些题做成限时测验——面试版就是肌肉记忆。
前端开发 · 中等

Cookie session和JWT怎么选?

内部单体我选Redis里的服务端session——注销一个DEL搞定,cookie也小。微服务或多region我走JWT——没有共享session store要协调。不管哪种,token放httpOnly+Secure+SameSite=Lax的cookie,绝不放localStorage——任何XSS都能偷。JWT最大坑是注销:要即刻"踢下线"就得维护黑名单,那"无状态"的卖点就破功了。标准套路:15分钟access JWT+7天refresh在httpOnly cookie,每次刷新轮换refresh,这样偷到的refresh只能用一次。
💡 提示:能不用localStorage存JWT就不用——XSS直接game over。;SameSite=Lax是2026年的合理CSRF默认(多数浏览器已默认)。;即答侠把这题拆成"状态 vs 扩展 vs 注销"三个维度——结构化答,不堆术语。
前端开发 · 中等

怎么防XSS和CSRF?两者区别?

XSS=代码注入进你的页面;CSRF=浏览器被骗去发已登录的请求。XSS我会:React/Vue模板默认转义解决90%,严格Content-Security-Policy兜底,dangerouslySetInnerHTML必须配DOMPurify。认证cookie设httpOnly,XSS成功也偷不到token。CSRF我会:会话cookie设SameSite=Lax(2026基线),状态变更接口加CSRF token在X-CSRF-Token header——攻击者跨站读不到cookie所以也原样echo不回来。优先级:XSS更严重因为它拿下你的页面;CSRF只能打到接口暴露的操作。
💡 提示:CSP用`strict-dynamic`+nonce是金标准。;GET不要改状态——大部分读场景直接免疫CSRF。;即答侠把每个防御配上它挡的那种攻击——面试官要的是因果,不是清单。
前端开发 · 中等

讲讲浏览器渲染流水线。哪些CSS触发重排哪些只触发重绘?

流水线从上到下:JS改DOM→样式重算→布局→绘制→合成。改几何属性(width/top/font-size)触发布局,这一步最贵因为会级联。改颜色/可见性触发绘制跳过布局,便宜些。transform/opacity两步都跳——走合成线程,60fps动画就靠它们。实操:动画用`transform: translateX()`而不是`left: Xpx`;批量先读后写避免layout thrashing(每次写都使布局失效,后续读会强制同步布局)。重动画的元素用will-change: transform提升成独立图层。教新人一句话:如果在做位移动画,十有八九你该用transform。
💡 提示:Performance面板→Rendering→Layout Shift Regions,直接看实际影响。;循环里调getBoundingClientRect()会强制同步布局——layout thrashing经典来源。;即答侠把流水线配上具体"该这样/别那样"的代码片段——答得实际,不像照本宣科。
前端开发 · 中等

防抖和节流有什么区别?手写一下。

防抖是"等安静",节流是"限速率"。搜索框我防抖300ms——用户停下才发请求。滚动动画或粘顶逻辑我节流16ms(约60fps)——手势期间要持续更新,不能沉默。Lodash有现成的,但自己写也就10行闭包:防抖存timerId每次重置,节流存lastTime判断时间够了才执行。常见选项:`leading`(先执行再冷却)、`trailing`(末尾执行)、防抖的`maxWait`(别推迟太久)。
💡 提示:requestAnimationFrame比setTimeout更适合"节流到帧率"——跟渲染周期同步。;React里用useRef存timer跨渲染;别每次render都重新造debounce函数。;即答侠把两种实现背熟——白板上不会在闭包上卡壳。
前端开发 · 中等

什么是闭包?讲讲经典的循环setTimeout坑。

闭包=函数+它出生那个作用域,即便外层函数返回了也还能访问那些变量。经典题:var循环+setTimeout回调。var是函数作用域,三个回调引用同一个i,执行时i已经是3,所以3,3,3。换成let每轮都是新binding,就是0,1,2。我故意用这个模式做数据私有——只暴露操作私有状态的方法,不用挂在对象上。内存注脚:DOM节点被闭包引用又活得比节点久就是经典泄漏,addEventListener最常见。
💡 提示:ES6模块免费给你私有性——纯为隐私用闭包现在用得少,但库里还常见。;DevTools→Memory→Heap snapshot里闭包是单独分类——排泄漏利器。;即答侠整理了闭包TOP5坑,一看形状就能认出来。
前端开发 · 中等

手写一个通过Promise/A+基础测试的Promise。

三态pending/fulfilled/rejected单向转移。构造函数里收集pending时入队的回调,resolve/reject时依次触发。难点在.then:必须返回新Promise、handler必须异步跑(queueMicrotask)、必须采纳handler返回值的状态——包括返回的是另一个thenable(2.3.3递归)。最后这条是大多数手写实现通不过兼容性测试的地方。我背了约40行的版本:class MyPromise带state/value/handlers[],resolve/reject遍历handlers;then()`new MyPromise`,在executor里queueMicrotask跑当前handler,把返回值resolve给新Promise。达不到生产级但核心测试能过。
💡 提示:用queueMicrotask不用setTimeout——匹配规范的微任务语义。;别忘了finally():不管状态都跑、不观察值、透传。;即答侠有40行模板放手边——白板时不会卡壳。
前端开发 · 中等

CSR、SSR、SSG、ISR怎么选?

我按内容类型分。营销页/博客:SSG。个性化看板:CSR或带用户级缓存的SSR。偶尔更新价格的商品页:ISR+60秒revalidate。高动态feed:SSR+边缘缓存。经典错误是听说SSR对SEO好就全SSR——服务器和TTFB都遭罪。问自己一句话:"这页每人要多新":永远不新就SSG,能陈旧就ISR,每次请求就SSR,登录后才要就CSR。Next.js App Router让我按路由混搭,最终站点就是每页按需调过的拼盘。
💡 提示:测TTFB和LCP——直接对应SSR vs SSG取舍。;流式SSR(React Server Components)在削弱SSR的慢TTFB短板。;即答侠给了按内容类型的决策树——面试官爱听结构化答案。
前端开发 · 中等

为什么hook必须顶层调用?讲讲useEffect生命周期。

React按组件把hook放数组里,按调用顺序标识——`useState`位置0、`useEffect`位置1。把hook塞进if,两次渲染调用顺序不一样,state绑错slot。这就是"顶层调用"规则来源。useEffect在commit后:mount跑effect、update先cleanup再effect、unmount只cleanup。经典坑:stale closure——effect捕获render时的state,interval或订阅永远看到旧值除非列全依赖。exhaustive-deps ESLint规则能抓;我从不无理由关它。useMemo缓存贵的计算,useCallback缓存稳定函数引用传给memo子组件——乱用两者得不偿失,memoization本身也有成本。
💡 提示:依赖要稳,就在产生处用useCallback/useMemo包。;useRef是"我要这个值但不想重渲染"的逃生舱。;即答侠练了5个stale closure场景——一眼识别。
前端开发 · 中等

讲讲Core Web Vitals。挑一个说说怎么优化。

三个指标:LCP(绘制速度)、INP(响应性)、CLS(稳定性)。最近一个项目LCP 4.2s——主因是首屏hero图等组件hydrate完才加载。我加了`<link rel="preload" as="image">`、转AVIF、CDN带responsive srcset,LCP降到1.8s。INP我会审计主线程——收益多来自大bundle拆分、非关键hydration延迟(React.lazy、islands)、hash/parse丢web worker。CLS多是傻问题:广告和图没预留高度。`width/height`属性或`aspect-ratio` CSS解决90%。三个指标我都上生产RUM;Lighthouse实验室分骗长尾。
💡 提示:INP于2024年3月正式Core Web Vital——FID已退休。资源早于2024还在讲FID就过时了。;PageSpeed Insights同屏展示实验室和线上数据——线上才影响SEO。;即答侠附了真实项目优化前后的数字——答起来像干过的,不是看过的。
前端开发 · 中等

浏览器从输入 URL 到页面展示发生了什么?重点讲讲渲染流程中的关键路径和性能优化点。

整体流程八步:1) DNS 解析(本地缓存 → hosts → 系统 DNS → 递归查询);2) TCP 三次握手(HTTPS 多一次 TLS);3) 发送 HTTP 请求;4) 服务端响应 HTML;5) 浏览器解析 HTML 构建 DOM;6) 并行下载 CSS/JS/图片,解析 CSS 构建 CSSOM;7) DOM + CSSOM 合并为 Render Tree,布局(Layout)计算几何,绘制(Paint)分层栅格化;8) GPU 合成上屏。 关键路径优化:a) 减少阻塞:CSS 放 head(避免 FOUC),JS 用 defer/async(defer 等 DOM 解析完按序执行,async 加载完立即执行);b) 关键 CSS 内联(critical CSS),减少首屏渲染等待;c) 图片用 lazy load + WebP/AVIF + responsive srcset;d) 启用 HTTP/2 多路复用,合并小请求;e) 资源预加载:preload(关键资源)、prefetch(下一页)、preconnect(三方域名);f) 减少回流重绘:批量 DOM 操作、transform/opacity 走 GPU 合成、避免布局抖动(连续读写 offsetTop 会触发同步布局)。 面试时面试官常追问'白屏时间' vs 'FCP' vs 'LCP' vs 'TTI' 这几个指标,要能讲清。
💡 提示:八步流程不能漏,字节前端面非常爱考;defer vs async 必须能讲清差别;性能指标(FCP/LCP/TTI)是 Web Vitals 必考
前端开发 · 中等

React 16 引入 Fiber 架构解决了什么问题?讲讲时间分片、调度和并发模式。

Fiber 解决的核心问题:React 15 的 reconciliation 是一次性递归,DOM 树深时会长时间占用主线程,导致掉帧、动画卡顿、输入延迟。Fiber 把工作单元拆成可中断的链表节点(双向 + child + sibling + return),每处理一个节点就检查 deadline(借助 requestIdleCallback / 自研 Scheduler 的 MessageChannel),时间不够就让出主线程,下一帧空闲时继续。 这就是时间分片(time slicing):把渲染工作切成 5ms 一片,中间让浏览器处理输入和动画。调度器(Scheduler)给任务分优先级:Immediate(同步)、UserBlocking(250ms)、Normal(5s)、Low(10s)、Idle,高优任务可以打断低优任务。 并发模式(Concurrent Mode,React 18 正式)带来 startTransition(标记非紧急更新)、useDeferredValue(延迟某状态的渲染)、Suspense for data fetching 等能力。典型场景:输入框打字立即更新 input,搜索结果列表用 startTransition 包起来允许打断,体验是'打字不卡顿'。 坑点:concurrent 下组件可能多次渲染(渲染阶段被中断丢弃),所以渲染阶段必须纯,副作用要放 useEffect。
💡 提示:Fiber 的'可中断链表节点'是和递归方案的本质区别;时间分片 + 优先级调度是两个组合机制,别讲混;Concurrent Mode 的 startTransition 要有具体使用场景
前端开发 · 简单

CSS 实现垂直居中有几种方式?优劣对比一下。

6 种主流方式。1) Flex:父元素 display: flex; align-items: center; justify-content: center;。最简洁、最现代,IE11 部分支持。2) Grid:父元素 display: grid; place-items: center;。比 Flex 还简洁(一行搞定双轴居中),现代浏览器全支持。3) 绝对定位 + transform:子元素 position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);。不需要知道子元素尺寸,兼容性好(IE9+)。4) 绝对定位 + margin 负值:子元素已知宽高,margin-top/left 负一半。古老但管用。5) line-height = height:仅适用单行文字,简单。多行加 display: table-cell; vertical-align: middle;。6) padding 撑开:对称 padding 实现近似居中,适合纯文本卡片。 场景选择:现代项目优先 Flex(横竖灵活、IE11 可用);如果父元素只放一个子元素并双轴居中,Grid 最优;不知道子元素尺寸用 transform 方案;legacy 项目用 table-cell。 坑点:Flex 子元素 align-self 可以覆盖 align-items,transform 居中可能在动画时出现亚像素模糊(加 will-change: transform 或者用整数像素)。
💡 提示:6 种至少要能讲 4 种,Flex/Grid 优先;对比要带'IE 兼容性'+'是否需要知道子元素尺寸'两维;transform 亚像素模糊是面试官爱追问的细节
前端开发 · 困难

深入讲一下 React Fiber 架构,为什么 React 要从 Stack Reconciler 改成 Fiber?Fiber 如何实现可中断渲染?

**Stack Reconciler 的问题**: React 15 的 reconciliation 是递归调用,从根组件递归向下 diff,调用栈一旦开始就无法中断。组件树深时主线程被阻塞,长任务卡 UI,输入响应延迟。比如 10000 个节点的 diff 可能跑 200ms+,期间用户点击/输入完全无响应。 **Fiber 核心改造 (React 16)**: 1. **数据结构**:把递归的 Virtual DOM 变成链表式 Fiber 树。每个 Fiber 节点有 `child`/`sibling`/`return` 三个指针,可以像树遍历一样手动控制,不依赖调用栈。 2. **双缓冲**:current 树和 workInProgress 树,commit 时直接切换指针 (类似 GPU 双 buffer),避免半完成状态被渲染。 3. **可中断**:每处理完一个 Fiber 单元 (workLoop),检查 `shouldYield()` (基于 5ms 时间片),超时就把控制权交还浏览器,等下次空闲再继续。 4. **优先级调度**:通过 Scheduler 包给不同更新打优先级 (Immediate/User-blocking/Normal/Low/Idle)。高优先级更新可以打断低优先级的渲染。 **实现可中断的关键**: - **两阶段提交**:render phase (可中断, 纯计算) + commit phase (不可中断, 真实 DOM 操作)。 - **requestIdleCallback 思路**:实际用 MessageChannel 模拟 (因为 rIC 兼容性差且不够精确),5ms 时间片。 - **work in progress 复用**:如果某次 render 被中断,下次可能从头开始 (新更新进来时) 或继续 (无新更新)。 **带来的能力**: - Suspense for data fetching - Concurrent Mode / Transitions (React 18) - 自动批处理 - useTransition / useDeferredValue 等 hook **面试加分点**:能画出 Fiber 链表结构图;能讲清楚为什么 commit 不可中断 (DOM 操作中断会被用户看到中间态);能对比 Vue 3 的方案 (Vue 用响应式 + 静态提升,思路不同)。
💡 提示:讲清楚 Stack 的痛点是关键;Fiber 链表结构 + 三指针(child/sibling/return) 必提;render/commit 两阶段必须区分;5ms 时间片细节加分
前端开发 · 中等

TypeScript 高级类型:Conditional Types、Mapped Types、Template Literal Types 各举一个实战场景。

**1. Conditional Types**: 语法 `T extends U ? X : Y`,基于类型条件返回不同类型。 **实战场景**:写 API response 类型,根据请求 method 自动推断返回值。 ```typescript type ApiResponse<M extends 'GET' | 'POST'> = M extends 'GET' ? { data: User[] } : { id: string; success: boolean }; const getResp: ApiResponse<'GET'> = { data: [...] }; const postResp: ApiResponse<'POST'> = { id: '123', success: true }; ``` **配合 infer**:从 Promise 抽取内部类型。 ```typescript type Awaited<T> = T extends Promise<infer U> ? U : T; type X = Awaited<Promise<string>>; // string ``` **2. Mapped Types**: 语法 `{ [K in keyof T]: ... }`,遍历类型的所有 key 生成新类型。 **实战场景**:把所有字段变成 optional 用于 PATCH 接口。 ```typescript type PartialUpdate<T> = { [K in keyof T]?: T[K] }; interface User { id: string; name: string; age: number; } type UserPatch = PartialUpdate<User>; // { id?: string; name?: string; age?: number } ``` **带 modifier**:去掉 readonly。 ```typescript type Mutable<T> = { -readonly [K in keyof T]: T[K] }; ``` **实战 2**:把所有方法字段变成 Promise (适配异步代理)。 ```typescript type Async<T> = { [K in keyof T]: T[K] extends (...args: infer A) => infer R ? (...args: A) => Promise<R> : T[K]; }; ``` **3. Template Literal Types** (TS 4.1+): 语法 ``${T}-${U}``,字符串模板可以参与类
💡 提示:infer 关键字必须能讲清楚;Mapped Types 配合 modifier (+/-readonly, ?) 是高频;Template Literal 路由推导是 tRPC 经典用法;举库的真实例子加分
前端开发 · 中等

微信小程序的双线程架构是什么?为什么这么设计?跟 H5/React Native 有什么区别?

**1. 双线程架构 (Logic + Render)**: 小程序运行时分成两个独立线程: - **逻辑层 (Logic Layer)**:JS 代码跑在这里。iOS 用 JavaScriptCore,Android 用 V8。**没有 window/document/DOM API**。 - **视图层 (Render Layer)**:用 WebView 渲染 WXML (类 HTML) + WXSS (类 CSS)。 - 两层通过 **JSBridge** (本质是 native 中转) 通信,数据序列化为 JSON 传递。 ``` ┌─────────────┐ ┌─────────────┐ │ Logic │ │ Render │ │ (JSCore/V8) │←→ │ (WebView) │ │ - app.js │ JSBridge │ │ - page.js │ │ - WXML/WXSS │ └─────────────┘ └─────────────┘ ↓ ↓ Native API (微信) ``` **2. 为什么这么设计**? **a) 安全性**:JS 代码不能直接操作 DOM,防止开发者用 `document.write` 注入恶意脚本 / 跳转外部页面,破坏微信生态。 **b) 性能隔离**:JS 计算密集不会阻塞渲染,反之亦然。 **c) 跨平台一致**:双线程模型在 iOS/Android 表现一致,因为视图层都是 WebView。 **d) 平台管控**:微信可以拦截、审计、灰度更新所有 API 调用,因为必须经过 JSBridge。 **3. 跟 H5 区别**: - H5 是单线程 (JS 和渲染都在主线程)。 - H5 有完整 DOM/window API,灵活但可被滥用。 - H5 加载慢 (首屏 HTML/CSS/JS 下载);小程序预下载离线包,启动 < 1s。 - H5 无原生能力 (摄像头 / 蓝牙) 需要 JSAPI 桥接;小程序统一 wx.* 接口。 **4. 跟 React Native 区别**: - RN 也是双线程 (JS + Native UI thread),思路类似。 - 但 RN 视图是**原生组件** (UIView/Android View),小程序视图是 WebView 内的 HTML。 - RN 性能更高 (原生渲染),小程序灵活性好 (Web 技术栈)。 - RN 包大 (要带 RN runtime),小程序包限制 2MB (单包) / 20MB (分包)。 **5. 双线程的代价 (开发者要警惕)**: **a) setDa
💡 提示:双线程图必须能画或描述清楚;JSBridge 通信延迟是开发陷阱;setData 性能问题是高频面试点;Skyline 是 2023+ 新东西加分