以下是前端面试中常遭遇的"手撕"基础题目汇总,涵盖 HTML→JS→Vue→React,每题附经典实现/原理解析,可现场答题或后端总结。
HTML 基础题 📝
-
语义化卡片(Semantic Card + ARIA)
html<article class="card" aria-label="Product Card"> <header> <h2 class="card-title">超轻量可折叠水杯</h2> </header> <section class="card-body"> <p class="description">不占空间,随时随地喝水</p> <button aria-label="Add to cart">🛒</button> </section> </article>
✅ 掌握
<main>
,<article>
,<section>
,<header>
等 HTML5 语义标签;为无障碍辅助技术提供兼容(通过aria-label
); -
Sticky Footer 实现(Flex 布局 +
min-height: 100vh
)html<body class="page"> <main class="content">...多内容...</main> <footer class="footer">© 2025 Demo 公司</footer> </body>
csshtml, body { margin:0; padding:0; } .page { min-height:100vh; display:flex; flex-direction:column; } .content { flex:1; } .footer { height:60px; background:#f3f3f3; }
-
自定义元素 + Native Form 验证示例
html<label for="age">年龄<label><input id="age" type="number" min="1" max="120" required> <button onclick="form.reportValidity()">提交</button>
✅ 演示如何结合自定义
type
、required
以及reportValidity()
显示原生错误提示。
JavaScript 常考题 💡
手写防抖 & 节流
js
function debounce(fn, ms = 300) {
let timer;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), ms);
};
}
function throttle(fn, ms = 300) {
let last = 0, timer = null;
return function(...args) {
const now = Date.now();
if (now - last < ms) {
clearTimeout(timer);
timer = setTimeout(() => {
last = Date.now();
fn.apply(this, args);
}, ms - (now - last));
} else {
last = now;
fn.apply(this, args);
}
};
}
✅ 面试时经典考察滚动、输入频率控制函数,应理解 clearTimeout
的作用与首次触发/尾触发的区别。文中提到防抖 vs 节流面试解析非常常见 (掘金)。
Promise.all()
简易版
js
Promise.myAll = function(iterable) {
return new Promise((resolve, reject) => {
const arr = Array.from(iterable);
if (arr.length === 0) return resolve([]);
const res = new Array(arr.length);
let count = 0;
arr.forEach((p, i) => {
Promise.resolve(p)
.then(val => {
res[i] = val;
count += 1;
if (count === arr.length) resolve(res);
})
.catch(reject);
});
});
};
✅ 面试中对实现并发控制、失败短路、位置保持非常常见 (大厂面试每日一题, 思否)。
深拷贝(DeepClone)示例
js
function deepClone(obj, map = new WeakMap()) {
if (obj === null || typeof obj !== 'object') return obj;
if (map.has(obj)) return map.get(obj);
const clone = obj instanceof Date ? new Date(obj)
: obj instanceof RegExp ? new RegExp(obj)
: Array.isArray(obj) ? []
: {};
map.set(obj, clone);
Reflect.ownKeys(obj).forEach(key => {
clone[key] = deepClone(obj[key], map);
});
return clone;
}
✅ 要处理循环引用、特殊对象、保持原型链与不可枚举属性等深度拷贝复杂性 (ExplainThis)。
事件循环 + 宏任务/微任务机制解析(经典题)
题目示例:
js
console.log('A');
setTimeout(() => console.log('B'), 0);
Promise.resolve().then(() => console.log('C'));
console.log('D');
输出结果: A D C B
解析: 浏览器事件循环会先执行同步代码(A
, D
),然后将 .then
放入微任务队列(优先级高),立即执行(C
),最后执行宏任务队列中的 setTimeout
回调(B
) (CSDN博客)。
✅ 面试常見用以检测对 JS 单线程、事件循环模型与 async/await
背后原理的掌握。
Vue 基础题目 🧪
-
生命周期顺序与场景适用:
beforeCreate → created → beforeMount → mounted → beforeUpdate → updated → beforeUnmount/ destroyed
✔ 可用于初始化数据、手动注册订阅、第三方插件挂载、子组件交互等场景 (ExplainThis)。
-
手写简化
v-model
指令支持<input>
双向绑定:jsapp.directive('model', { beforeMount(el, binding, vnode) { el.value = binding.value; el.addEventListener('input', e => { vnode.context[binding.expression] = e.target.value; }); }, updated(el, binding) { if (el.value !== binding.value) el.value = binding.value; } });
✔ 考察
listener + props sync
,理解update
hook 中避免老值 stale problem。 -
Vue 的响应式原理(简版伪代码):
- 使用
Object.defineProperty
拦截 getter/setter,收集依赖; - 每个组件对应一个
Watcher
,监听属性变化后发起重新渲染; Dep.notify()
通知所有 watcher 更新视图;- Vue 的更新机制是"push + diff"(比 React 更精细);如果再触发展现 Virtual DOM diff,减少不必要操作 (cnblogs.com)。
- 使用
-
nextTick()
原理:- Vue 会维护一个更新队列,当数据变化时,使用
Promise.resolve().then()
把回调排入微任务; - 在本轮更新完成之后且 DOM patch 结束前,执行所有 queued callbacks ------ 就是
Vue.nextTick(fn)
的实现基础 (cnblogs.com)。
- Vue 会维护一个更新队列,当数据变化时,使用
React 实战题 🎯
setState
是同步还是异步?
✅ React 16 以前:
- 在 React 合成事件或生命周期内多个 setState 会批量异步执行;
- 在原生事件/
setTimeout
中则立即同步执行(无批量); isBatchingUpdates
控制逻辑状态,决定是马上更新还是放入队列延迟执行 (interview.poetries.top, interview.poetries.top)。
✅ React 18+(带automatic batching):
- 默认所有 setState 都批量异步执行,跨同步边界也可合并。
手写精简版 useSetState
Hook 用法:
js
function useSetState(initial) {
const [state, setState] = React.useState(initial);
const setMerge = React.useCallback(partial => {
setState(prev => typeof partial === 'function' ? { ...prev, ...partial(prev) } : { ...prev, ...partial });
}, []);
return [state, setMerge];
}
✔ 常用于类组件迁移或处理中间复杂状态对象。
自定义 Hook ------ "usePrevious" 用来获取上一次的 props / state 值:
js
function usePrevious(value) {
const ref = React.useRef();
React.useEffect(() => {
ref.current = value;
});
return ref.current;
}
推荐答题思路 vs ⚙️ 笔试技巧
- 🎯 原理结合代码实现 :例如讲
Promise.all
的状态控制步骤时,要同时写出.then/.catch
逻辑; - 🎯 边界处理 :空输入、循环引用、取消节流/防抖、
finally
支持; - 🎯 防坑措施 :写内容前先声明假设,如
arguments
,this
包含性、map.has()
; - 🎯 真实业务场景中的演练:例如:"如果输入框提交 API 想要防抖 + 防止重复提交,你会怎么做?"
最后一击:答题包装建议
步骤 | 做法 |
---|---|
✅ 场景定位 | "这个题是考异步模型 / 响应式设计 / 事件绑定机制。" |
✅ 核心原理 | 简练说明背后机制,宏/微任务、依赖追踪、渲染批次等。 |
⚠️ 关键限制条件 | 讨论 edge-case、API 兼容性、性能影响等。 |
💬 输出可交付物 | 写出刚才例子代码时,模拟如何现场写白板步骤,用注释提升可读性(why) |
掌握以上这四大模块的题型(HTML 布局+JavaScript 常见算法+Vue 响应式系统+React 状态更新策略),你在面试现场就可以胸有成竹地写出代码、讲清原理、回答补充问题。祝你面试拿 offer,写题不打烊!
前端面试中的场景题旨在考察候选人对实际开发问题的理解与解决能力。以下是一些高频场景题及其解析,供您参考:
以下是前端面试中HTML、JS、Vue、React的常考基础题目详解及代码解析,结合高频考点和实战需求整理:
一、HTML 核心考点
-
语义化标签
• 作用:提升代码可读性、SEO优化(如
、 替代
)• 示例:
网站头部 独立内容区 版权信息 -
本地存储
• 区别:
◦ localStorage:永久存储,同源共享
◦ sessionStorage:会话级存储,标签页关闭即清除
• 代码:
localStorage.setItem('key', 'value');
console.log(localStorage.getItem('key')); // 'value'
二、JavaScript 高频手写题
-
闭包与内存泄漏
• 原理:内层函数引用外层作用域变量(如计数器)
• 风险:未释放的变量导致内存泄漏
• 代码:
function createCounter() {
let count = 0;
return () => ++count;
}
const counter = createCounter();
counter(); // 1
-
Promise.all 实现
• 功能:并行执行多个Promise,全部成功返回结果数组,任一失败立即拒绝
• 代码:
Promise.myAll = (promises) => {
let results = [], count = 0;
return new Promise((resolve, reject) => {
promises.forEach((p, i) => {
Promise.resolve§.then(res => {
results[i] = res;
if (++count === promises.length) resolve(results);
}).catch(reject);
});
});
};
-
防抖(Debounce)
• 场景:搜索框输入停止后触发请求
• 代码:
function debounce(fn, delay) {
let timer;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
// 使用:window.addEventListener('resize', debounce(handleResize, 200));
三、Vue 重点解析
-
响应式原理
• Vue2:Object.defineProperty 监听属性,需递归遍历对象
• Vue3:Proxy 代理整个对象,支持动态新增属性
• 示例:
// Vue3 Proxy 实现
const proxy = new Proxy(obj, {
get(target, key) { /* 依赖收集 / },
set(target, key, value) { / 触发更新 */ }});
-
Composition API vs Options API
• 优势:逻辑复用更灵活(如抽取useCounter函数)
• 代码对比:
// Composition API
setup() {
const count = ref(0);
const double = computed(() => count.value * 2);
return { count, double };
}
四、React 核心考点
-
Hooks 依赖项陷阱
• 错误示例:useEffect 空依赖导致闭包捕获旧值
• 正确做法:明确依赖项数组
useEffect(() => {
console.log(count);
}, [count]); // 依赖项为count
-
Fiber 架构
• 目的:解决同步渲染阻塞问题,实现可中断更新
• 原理:将任务拆分为小单元,通过requestIdleCallback调度
-
虚拟DOM与Key属性
• Diff算法:key 帮助识别节点,避免不必要的重渲染
• 错误用法:用数组下标作为key(数据变化时导致渲染错误)
五、跨框架通用考点
-
组件通信
• Vue:props/$emit、provide/inject、Vuex
• React:props、Context API、Redux
-
性能优化
• 通用方案:
◦ 图片懒加载(IntersectionObserver API)
◦ 虚拟列表(仅渲染可视区域)
◦ 代码分割(Webpack的import()动态加载)
面试技巧
• 手写题注意:先口述思路再写代码,处理边界条件(如Promise.all传入空数组)
• 框架原理:准备1-2个深度用例(如Vue3的Tree Shaking如何减少打包体积)
• 项目经验:用STAR法则描述技术选型(如为何选Redux而非Context)
系统学习建议:针对薄弱点重点突破(如JS异步/闭包、框架原理),配合手写代码练习4。
1. 如何高效渲染一万条数据?
面对大量数据渲染时,可采用以下策略:
- 虚拟列表:仅渲染可视区域的数据,提升性能。
- 分页加载:将数据分批加载,减少初始渲染压力。
- 分片渲染 :利用
requestAnimationFrame
或setTimeout
将渲染任务分批执行,避免主线程阻塞。 - 使用
createDocumentFragment
:在批量插入 DOM 时,先将元素添加到文档片段中,再统一插入,提高效率。(adjfks.github.io)
2. 图片懒加载的实现方式
图片懒加载有助于优化页面加载性能,常见实现方式包括:
- IntersectionObserver API:监听元素是否进入视口,进入后加载图片。
- 滚动事件监听 :在不支持 IntersectionObserver 的环境中,监听滚动事件,结合
getBoundingClientRect
判断图片是否进入视口。(adjfks.github.io)
3. 实现无感刷新 Token 的机制
为了提升用户体验,可在 Token 即将过期时自动刷新:
- 请求前判断:在每次请求前检查 Token 是否即将过期,若是,则先刷新 Token。
- 响应拦截器处理:在响应拦截器中处理 Token 过期的情况,刷新后重试原请求。
- 请求队列管理 :在刷新过程中,将其他请求暂存,待刷新完成后再统一发送,避免重复刷新。(adjfks.github.io)
4. 实现扫码登录功能
扫码登录的基本流程如下:
- PC 端请求生成二维码,服务器返回唯一标识。
- 移动端扫描二维码,获取标识后发送登录请求。
- 服务器验证后,更新二维码状态。
- PC 端通过轮询、长轮询或 WebSocket 等方式监听登录状态,完成登录。(adjfks.github.io)
5. 实现上拉加载与下拉刷新
在移动端常见的交互方式:
- 上拉加载:监听滚动事件,判断是否接近底部,触发加载更多数据。
- 下拉刷新:监听触摸事件,判断下拉距离,达到阈值后触发刷新操作。
6. 移动端实现 1px 线条
由于高分辨率屏幕的存在,1px 线条可能显示为多像素,可通过以下方式实现:
- 使用伪元素结合
transform: scale
:创建伪元素,设置其宽度为 1px,然后通过缩放实现视觉上的 0.5px 线条。(adjfks.github.io)
7. 网页加载缓慢的排查方法
当网页加载缓慢时,可从以下方面进行排查:
- 浏览器性能分析:使用开发者工具查看资源加载时间、脚本执行时间等。
- 网络请求分析:检查是否存在慢速请求或失败的请求。
- 资源优化:压缩图片、合并文件、使用 CDN 等。
- 服务端性能 :检查服务器响应时间、数据库查询效率等。(adjfks.github.io)
以上内容涵盖了前端面试中常见的场景题及其解决方案。建议在准备面试时,结合实际项目经验,深入理解每个场景的实现原理和优化策略。如需更详细的解析或其他场景题的解答,欢迎继续提问。(GitHub)
以下是对先前列出七个前端面试场景题的系统化深入解读,每个题目附带典型代码示例与注意事项,适用于面试答题或实际开发参考。
1. 高效渲染上万条数据 ------ 虚拟列表(Virtual Scrolling/Windowing)
🧠 核心思路
- 页面仅渲染可视区域内的列表项,不把整个数据都注入 DOM,避免重绘/回流卡顿。
- 通过一个 占位容器 (phantom div)模拟整个滚动高度,内部真实渲染区通过
transform / position: absolute
动态定位内容位置(CSDN博客)。
💻 典型实现(React + 固定高度版本)
jsx
// VirtualList.jsx
import React, { useRef, useState, useEffect, useCallback } from 'react';
export default function VirtualList({ data, itemHeight, containerHeight }) {
const totalHeight = data.length * itemHeight;
const containerRef = useRef(null);
const [scrollTop, setScrollTop] = useState(0);
const onScroll = useCallback(e => {
setScrollTop(e.currentTarget.scrollTop);
}, []);
const startIndex = Math.floor(scrollTop / itemHeight);
const visibleCount = Math.ceil(containerHeight / itemHeight) + 1;
const slice = data.slice(startIndex, Math.min(data.length, startIndex + visibleCount));
return (
<div
ref={containerRef}
style={{ height: containerHeight, overflowY: 'auto', position: 'relative' }}
onScroll={onScroll}
>
<div style={{ height: totalHeight, position: 'relative' }}>
<div
style={{
position: 'absolute',
top: startIndex * itemHeight,
left: 0, right: 0
}}
>
{slice.map((item, idx) => (
<div key={startIndex + idx} style={{ height: itemHeight }}>
{item.name}
</div>
))}
</div>
</div>
</div>
);
}
说明:
containerHeight
可取视口高度或父容器的固定高度;startIndex
和visibleCount
控制数据 slice;- 滚动事件处理可加节流或
requestAnimationFrame
降频。
📦 动态高度项 & 高阶实现
- 若列表项高度不固定,可采用 预估高度 + Height Cache 策略,利用
ResizeObserver
动态更新实际高度,再维护累计高度数组以准确定位(CSDN博客)。 - 也可直接使用成熟第三方库,例如
react-window
、vue-virtual-scroller
等。
✅ 面试答题要点速记
- 解释原理:占位层 + 可视窗口 + 滚动监听 + slicing。
- 时间复杂度:O(可视数量) 而非 O(n),DOM 更新量大幅下降。
- 注意事项:处理边界(首尾条目)、动态高度项、首屏加载优化、首屏渲染数量阈值。
2. 图片懒加载(Lazy‑Load)
🧠 原理说明
- IntersectionObserver API 可以异步观察元素是否与视口相交,用于判断可见就加载图片,取代传统 scroll/resize 判断(Medium)。
- 如果浏览器不支持 IntersectionObserver,则 fallback 到滚动监听 +
getBoundingClientRect()
决定加载。
💻 示例代码(纯 JS)
html
<img class="js-lazy" data-src="..." src="data:,...tiny.jpg" alt="..." />
<script>
const lazyImgs = document.querySelectorAll('img.js-lazy');
if ('IntersectionObserver' in window) {
const io = new IntersectionObserver((entries, obs) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.classList.remove('js-lazy');
obs.unobserve(img);
}
});
}, { rootMargin: '200px' });
lazyImgs.forEach(img => io.observe(img));
} else {
const onScroll = () => {
lazyImgs.forEach(img => {
if (img.classList.contains('js-lazy')) {
const { top } = img.getBoundingClientRect();
if (top < window.innerHeight + 200) {
img.src = img.dataset.src;
img.classList.remove('js-lazy');
}
}
});
if ([...lazyImgs].every(img => !img.classList.contains('js-lazy'))) {
window.removeEventListener('scroll', onScroll);
}
};
window.addEventListener('scroll', onScroll, { passive: true });
onScroll();
}
</script>
📘 行业实践建议
- 加
rootMargin: "200px"
预加载缓冲区; - 图片本身使用
loading="lazy"
属性也可自动延迟加载; - React/Vue 中可封装
LazyImage
组件尽量复用(blog.logrocket.com)。
3. 无感刷新 Token 的机制(以 JWT + Axios 为例)
🧠 流程说明
- 前端每次请求前自动注入当前 access token。
- 若后端返回 access token 失效(如 HTTP 401/403),Axios 响应拦截器捕获错误。
- 前端发起一次 refresh token 请求换取新 access token(请确保 refresh token 安全存储,通常 httpOnly cookie)。
- 刷新成功后,重试原始请求;刷新失败则跳转登录或报错处理。
- 在刷新期间控制并发请求只发起一轮刷新请求,其余排队等待再执行(Medium)。
💻 示例代码(Axios 拦截器)
js
// api.js
import axios from 'axios';
const api = axios.create({
baseURL: '/api',
withCredentials: true
});
let isRefreshing = false;
let queue = [];
api.interceptors.request.use(async config => {
const token = localStorage.getItem('access_token');
if (token) config.headers.Authorization = `Bearer ${token}`;
return config;
});
api.interceptors.response.use(
res => res,
async err => {
const { config, response } = err;
if (!config._retry && response?.status === 401) {
config._retry = true;
if (!isRefreshing) {
isRefreshing = true;
try {
const { data } = await axios.get('/auth/refresh', { withCredentials: true });
localStorage.setItem('access_token', data.accessToken);
isRefreshing = false;
queue.forEach(cb => cb(null, data.accessToken));
queue = [];
} catch (refreshErr) {
isRefreshing = false;
queue.forEach(cb => cb(refreshErr));
queue = [];
// redirect to login
window.location = '/login';
}
}
return new Promise((resolve, reject) => {
queue.push((refreshErr, newToken) => {
if (refreshErr) {
reject(refreshErr);
} else {
config.headers.Authorization = `Bearer ${newToken}`;
resolve(api(config));
}
});
});
}
return Promise.reject(err);
}
);
export default api;
🗣 Reddit 小贴士摘录:
"Better to simply check if the token has expired on the client ... this way we are not wasting an HTTP request"(reddit.com)
✅ 面试答题要点
问题 | 回答 |
---|---|
存储 refresh token 安全吗? | 推荐 httpOnly cookie + 后端验证机制。 |
多请求时重复刷新怎么办? | 单例刷新 + 请求队列机制。 |
token 刷新失败如何处理? | 清 session/redirect 登录。 |
第三方库 | axios-auth-refresh 可提供模板机制(npmjs.com)。 |
4. 实现扫码登录流程(PC + Mobile)
🧠 常见方案
方 法 | 简介 |
---|---|
轮询 / 长轮询 | PC 端生成一个临时 session(如 UUID),轮询接口状态。若用户手机扫码确认,状态变更则登录。适合不想做 WebSocket 的系统。 |
WebSocket / Server-Sent Events | PC 端连接 WebSocket,二维码扫码后后端推送状态,PC 立即响应登录。交互更实时。 |
💻 长轮询伪代码示例(纯 JS)
js
// PC 端
async function pollStatus(uuid) {
while (true) {
const { data } = await fetch(`/auth/qr-status?uuid=${uuid}`).then(r => r.json());
if (data.status === 'confirmed') {
window.localStorage.setItem('session', data.session);
return;
}
if (data.status === 'expired') throw new Error('二维码过期');
await new Promise(r => setTimeout(r, 1500));
}
}
💻 WebSocket 示例(Node + 前端 WebSocket)
js
// 前端 PC 端
const ws = new WebSocket('wss://example.com/ws');
ws.onopen = () => ws.send(JSON.stringify({ type: 'launch', uuid }));
ws.onmessage = evt => {
const msg = JSON.parse(evt.data);
if (msg.type === 'login_success') {
localStorage.setItem('session', msg.session);
ws.close();
}
};
服务器在扫码后查到对应 uuid 会主动 ws.send({ type: 'login_success', session })
推给 PC 端。
📘 就业面试记要点
- 解释长轮询、WebSocket 的基本差别:实时性 vs 简单性(Medium);
- 安全考虑:UUID 唯一性、过期机制、防止扫码劫持;
- 使用 HTTPS + token 加密传输。
5. 上拉加载(Infinite Scroll)与下拉刷新(Pull‑to‑Refresh)
🧠 上拉加载(Infinite Scroll)
- 页面滚动到底部时自动加载下一页数据,通常配合懒加载使用;
- 检测
scrollTop + clientHeight >= scrollHeight - threshold
时触发加载(dev.to)。
js
// simple infinite scroll
window.addEventListener('scroll', async () => {
const { scrollY, innerHeight } = window;
const { scrollHeight } = document.documentElement;
if (scrollY + innerHeight >= scrollHeight - 200) {
await loadMore(); // 异步获取并 append DOM
}
});
🧠 下拉刷新(Pull‑to‑Refresh)
- 在移动端拖拽顶部触发刷新,使用 CSS
overscroll-behavior
和 touch 监听实现(blog.openreplay.com)。
css
/* CSS */
html, body { overscroll-behavior-y: contain; }
.pull-refresh { position: relative; transition: transform 0.3s; }
js
// JS 大致流程
let startY = 0, dist = 0, pulling = false;
const threshold = 60;
container.addEventListener('touchstart', e => { startY = e.touches[0].clientY; });
container.addEventListener('touchmove', e => {
const y = e.touches[0].clientY;
dist = y - startY;
if (dist > 0 && container.scrollTop === 0) {
pulling = true;
e.preventDefault();
pullrefreshEl.style.transform = `translateY(${Math.min(dist, threshold)}px)`;
}
});
container.addEventListener('touchend', async () => {
if (pulling) {
pullrefreshEl.style.transition = 'transform 0.3s';
if (dist >= threshold) {
pullrefreshEl.style.transform = `translateY(${threshold}px)`;
await doRefresh();
}
setTimeout(() => {
pullrefreshEl.style.transform = '';
pullrefreshEl.style.transition = '';
pulling = false;
}, 300);
}
});
📝 面试答题小记
- 什么场景用 Infinite Scroll?:社交 feed,商品列表。
- 缺点?:加载时底部卡顿、无法跳页、URL 不稳定。
- 如果页面内容混合无限加载和 pull‑to‑refresh,需额外处理状态冲突。
6. 移动端实现视觉上"1px"线条的技巧
🧠 背后原理
- 在高设备像素比(DPR)屏幕上,CSS
border: 1px
实际为多个物理像素,例如 iPhone 上 DPR=3,实际为 3px。 - 为保证视觉为"1px",通常用伪元素配
transform: scale
,缩放确定宽度正确(javascript.plainenglish.io),也可用border-width: 0.5px
兼容部分设备。
💻 示例代码
css
/* 通用 1px 线条 */
.hairline {
position: relative;
}
.hairline::after {
content: '';
position: absolute;
top: 0; left: 0; right: 0;
height: 100%;
border-top: 1px solid #ccc;
transform-origin: top;
}
@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 2dppx) {
.hairline::after {
transform: scaleY(0.5);
}
}
@media (-webkit-min-device-pixel-ratio: 3), (min-resolution: 3dppx) {
.hairline::after {
transform: scaleY(0.3333);
}
}
<div class="hairline"></div>
得到视觉精确的 1px 线条。- 注意:部分低 DPR(如 DPR=1.5)设备上精确度有限。
7. 网页加载缓慢的排查思路与工具
🛠 工具与分析指标
- 推荐用 Chrome DevTools 的 Network 面板 + Performance 面板 + Lighthouse 审核报告 组合进行排查(developers.google.com)。
- Network 面板 :检查 HTTP 请求数量、资源大小、缓存策略、HTTP status、水印等(developers.google.com)。
- Performance 面板 :分析 JS 执行时长、主线程阻塞、帧率(FPS)、内存泄漏等(web.dev)。
- Lighthouse 审核 :测算 LCP、CLS、INP、FCP、TBT 等核心指标,提供优化建议清单(en.wikipedia.org)。
📋 排查流程
- 首次打开页面,选择 "Disable cache + Throttle to Fast 4G" 模拟真实慢网络。
- 运行 Lighthouse 得出性能分数,重点查看哪些 audit failed。
- 切到 Network/T&M 面板定位 Largest contentful paint 所对应资源(卡在哪个资源加载)。
- 性能长片段 trace 尝试查找重排与渲染瓶颈。
- 结合代码定位问题(如第三方库体积、重复加载、未按需加载 JS/CSS)。
🧪 代码层面参考做法
js
// 前端可插入性能标记,用以测量关键函数执行时间
performance.mark('startInit');
// 初始化耗时操作...
performance.mark('endInit');
performance.measure('initTime', 'startInit', 'endInit');
console.log(performance.getEntriesByName('initTime'));
✅ 面试答题重点:
- 能提到具体指标如 LCP(最大内容渲染时间)、TBT(总线程阻塞)、INP(交互延迟)。
- 能解释优化思路:HTTP 压缩/缓存、代码拆分、图片延迟加载、tree‑shaking、避免首次渲染过多 DOM 等策略。
🔚 总结建议
- 每个场景题都可从 原理 + 核心代码实现 + 常见优化点 三个维度解答,面试中易获高分。
- 建议在真实项目或 CodeSandbox 中系统实践上述代码,并熟悉 Chrome DevTools 和 Lighthouse 的使用。
- 若您希望深入某一个场景(如动态高度虚拟列表、多终端 scan‑login 等),可随时告诉我,我可以给您更加针对性的代码讲解。
祝您面试顺利!