前端工程化与性能优化指南
一、Webpack 核心配置与原理
1.1 Webpack 基础配置手写
1.1.1 最小化 Webpack 配置
const path = require('path');
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename: '[name].[hash].js',
path: path.resolve(__dirname, 'dist'),
clean: true
}
};
1.1.2 完整开发环境配置
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { VueLoaderPlugin } = require('vue-loader');
const webpack = require('webpack');
module.exports = {
entry: {
main: './src/main.js',
vendors: './src/vendors.js'
},
output: {
filename: '[name].[hash:8].js',
path: path.resolve(__dirname, 'dist'),
publicPath: '/',
hashDigestLength: 8,
assetModuleFilename: 'assets/[hash][ext][query]',
clean: true
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
configFile: path.resolve(__dirname, '.babelrc'),
cacheDirectory: true
}
}
},
{
test: /\.css$/,
use: [
{ loader: 'style-loader' },
{
loader: 'css-loader',
options: {
modules: {
localIdentName: '[name]__[local]--[hash:base64:5]'
}
}
}
]
},
{
test: /\.scss$/,
use: [
{ loader: 'style-loader' },
{ loader: 'css-loader' },
{
loader: 'sass-loader',
options: {
implementation: require('sass')
}
}
]
},
{
test: /\.(png|jpg|jpeg|gif|svg)$/i,
type: 'asset/resource',
generator: {
filename: 'images/[hash][ext][query]'
}
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/i,
type: 'asset/resource',
generator: {
filename: 'fonts/[hash][ext][query]'
}
},
{
test: /\.vue$/,
loader: 'vue-loader'
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
filename: 'index.html',
inject: true,
minify: {
removeComments: true,
collapseWhitespace: true,
removeAttributeQuotes: true
}
}),
new VueLoaderPlugin(),
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('development'),
'process.env.VUE_APP_BASE_API': JSON.stringify('/api')
}),
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery',
'window.jQuery': 'jquery',
Vue: ['vue/dist/vue.esm.js', 'default']
})
],
resolve: {
extensions: ['.js', '.vue', '.json', '.css'],
alias: {
'@': path.resolve(__dirname, 'src'),
'vue$': 'vue/dist/vue.esm.js',
'components': path.resolve(__dirname, 'src/components'),
'utils': path.resolve(__dirname, 'src/utils')
},
modules: [
path.resolve(__dirname, 'src'),
'node_modules'
],
mainFields: ['browser', 'module', 'main']
},
devServer: {
static: {
directory: path.join(__dirname, 'public')
},
port: 8080,
open: true,
hot: true,
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
pathRewrite: { '^/api': '' }
}
},
historyApiFallback: true,
compress: true
},
optimization: {
splitChunks: {
chunks: 'all',
minSize: 20000,
maxAsyncRequests: 30,
maxInitialRequests: 30,
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
priority: -10
},
common: {
minChunks: 2,
name: 'common',
chunks: 'all',
priority: -20
}
}
},
runtimeChunk: 'single',
moduleIds: 'deterministic',
usedExports: true,
concatenateModules: true
},
target: 'web',
devtool: 'cheap-module-source-map',
stats: 'errors-warnings',
externals: {
vue: 'Vue',
react: 'React'
}
};
1.2 Webpack 高级配置
1.2.1 生产环境优化配置
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const PrerenderSPAPlugin = require('prerender-webpack-plugin');
module.exports = (env, argv) => {
const isProduction = argv.mode === 'production';
return {
mode: isProduction ? 'production' : 'development',
devtool: isProduction ? false : 'eval-source-map',
entry: {
app: './src/main.js'
},
output: {
filename: isProduction
? 'js/[name].[contenthash:8].js'
: 'js/[name].js',
path: path.resolve(__dirname, 'dist'),
publicPath: 'https://cdn.example.com/',
sourceMapFilename: '[file].map',
clean: true
},
module: {
rules: [
{
test: /\.css$/,
use: [
isProduction
? MiniCssExtractPlugin.loader
: { loader: 'style-loader' },
{ loader: 'css-loader' }
]
},
{
test: /\.(png|jpg|jpeg|gif|svg)$/i,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 8 * 1024
}
},
generator: {
filename: 'images/[name].[hash:8][ext]'
}
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
minify: isProduction ? {
removeComments: true,
collapseWhitespace: true,
removeAttributeQuotes: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true
} : false
}),
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:8].css',
chunkFilename: 'css/[name].[contenthash:8].chunk.css'
}),
new CleanWebpackPlugin(),
new PrerenderSPAPlugin({
staticDir: path.join(__dirname, 'dist'),
routes: ['/', '/about', '/contact'],
minify: {
collapseWhitespace: true,
removeComments: true
}
})
],
optimization: {
minimizer: [
new TerserPlugin({
terserOptions: {
parse: {
ecma: 2020
},
compress: {
drop_console: true,
drop_debugger: true,
dead_code: true,
booleans: true,
conditionals: true,
unused: true
},
mangle: {
safari10: true
},
output: {
comments: /@license/i
}
},
extractComments: false,
parallel: true
}),
new CssMinimizerPlugin({
minimizerOptions: {
preset: [
'default',
{
discardComments: { removeAll: true }
}
]
}
})
],
splitChunks: {
chunks: 'all',
maxInitialRequests: 5,
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
priority: 10
},
common: {
minChunks: 2,
name: 'common',
chunks: 'all',
priority: 5
},
vue: {
test: /[\\/]node_modules[\\/](vue|vue-router|vuex)[\\/]/,
name: 'vue-vendor',
chunks: 'all',
priority: 20
}
}
},
runtimeChunk: 'single',
moduleIds: 'deterministic',
usedExports: true,
concatenateModules: true
},
performance: {
hints: isProduction ? 'warning' : false,
maxEntrypointSize: 512000,
maxAssetSize: 512000
}
};
};
1.2.2 Webpack 插件手写
class CustomPlugin {
constructor(options = {}) {
this.options = options;
this.pluginName = 'CustomPlugin';
}
apply(compiler) {
compiler.hooks.compilation.tap(this.pluginName, (compilation) => {
compilation.hooks.processAssets.tapAsync(
{
name: this.pluginName,
stage: compilation.PROCESS_ASSETS_STAGE_ADDITIONAL
},
(assets, callback) => {
for (const [filename, asset] of Object.entries(assets)) {
console.log(`Generated asset: ${filename}`);
console.log(`Size: ${asset.size()} bytes`);
}
callback();
}
);
});
compiler.hooks.emit.tapAsync(this.pluginName, (compilation, callback) => {
const stats = compilation.getStats();
const assets = Object.keys(compilation.assets);
console.log(`Total assets: ${assets.length}`);
const manifest = {
buildTime: new Date().toISOString(),
assets: assets.map(name => ({
name,
size: compilation.assets[name].size()
}))
};
compilation.assets['manifest.json'] = {
source: () => JSON.stringify(manifest, null, 2),
size: () => JSON.stringify(manifest).length
};
callback();
});
compiler.hooks.afterEmit.tapAsync(this.pluginName, (compilation, callback) => {
console.log('Build completed and files emitted');
callback();
});
compiler.hooks.done.tapAsync(this.pluginName, (stats, callback) => {
const buildInfo = stats.toJson();
console.log('Build done:', {
time: buildInfo.time,
assets: buildInfo.assets.length,
warnings: buildInfo.warnings.length,
errors: buildInfo.errors.length
});
callback();
});
}
}
module.exports = {
plugins: [
new CustomPlugin({
verbose: true
})
]
};
1.2.3 Webpack Loader 手写
function simpleLoader(source) {
const transformed = source.replace(/旧文本/g, '新文本');
return transformed;
}
function fullLoader(source, inputSourceMap) {
const options = this.getOptions();
const callback = this.async();
callback(null, transformedSource, sourceMap);
}
const { getOptions } = require('loader-utils');
const { validate } = require('schema-utils');
const schema = {
type: 'object',
properties: {
name: { type: 'string' },
age: { type: 'number' }
},
additionalProperties: false
};
function loader(source) {
const options = getOptions(this);
validate(schema, options, {
name: 'MyLoader',
baseDataPath: 'options'
});
let transformed = source;
if (options.name) {
transformed = transformed.replace(/PROJECT_NAME/g, options.name);
}
return transformed;
}
const marked = require('marked');
function markdownLoader(source) {
const options = this.getOptions();
const html = marked(source, {
gfm: true,
breaks: options.breaks || false
});
const result = `
const html = ${JSON.stringify(html)};
export default html;
`;
return result;
}
function pitchLoader(remainingRequest, precedingRequest, data) {
if (someCondition) {
return '';
}
}
module.exports = markdownLoader;
module.exports.pitch = pitchLoader;
1.3 Webpack 核心原理
1.3.1 Tapable 插件系统原理
const { SyncHook, SyncBailHook, SyncWaterfallHook, SyncLoopHook } = require('tapable');
const { AsyncParallelHook, AsyncSeriesHook } = require('tapable');
class MyPlugin {
constructor() {
this.hooks = {
greet: new SyncHook(['name', 'age'])
};
}
register() {
this.hooks.greet.tap('Plugin1', (name, age) => {
console.log('Plugin1:', name, age);
});
this.hooks.greet.tap('Plugin2', (name, age) => {
console.log('Plugin2:', name, age);
});
}
call(name, age) {
this.hooks.greet.call(name, age);
}
}
class MyBailPlugin {
constructor() {
this.hooks = {
find: new SyncBailHook(['data'])
};
}
register() {
this.hooks.find.tap('CachePlugin', (data) => {
if (data.cache) {
return data.cache;
}
});
this.hooks.find.tap('FetchPlugin', (data) => {
return fetchFromNetwork(data);
});
}
call(data) {
const result = this.hooks.find.call(data);
console.log('Result:', result);
}
}
class MyWaterfallPlugin {
constructor() {
this.hooks = {
process: new SyncWaterfallHook(['initial'])
};
}
register() {
this.hooks.process.tap('Step1', (initial) => {
return initial + ' -> Step1';
});
this.hooks.process.tap('Step2', (preResult) => {
return preResult + ' -> Step2';
});
this.hooks.process.tap('Step3', (preResult) => {
return preResult + ' -> Step3';
});
}
call() {
const result = this.hooks.process.call('Start');
}
}
class MyAsyncPlugin {
constructor() {
this.hooks = {
load: new AsyncSeriesHook(['resource'])
};
}
register() {
this.hooks.load.tapAsync('FileLoader', (resource, callback) => {
fs.readFile(resource, (err, data) => {
if (err) return callback(err);
console.log('File loaded');
callback();
});
});
this.hooks.load.tapAsync('CacheLoader', (resource, callback) => {
checkCache(resource, (err, cached) => {
if (err) return callback(err);
console.log('Cache checked');
callback();
});
});
}
call(resource) {
this.hooks.load.callAsync(resource, (err) => {
if (err) console.error('Error:', err);
else console.log('All done');
});
}
}
class MyParallelPlugin {
constructor() {
this.hooks = {
parallel: new AsyncParallelHook(['data'])
};
}
register() {
this.hooks.parallel.tapAsync('Task1', (data, callback) => {
setTimeout(() => {
console.log('Task1 done');
callback(null, 'result1');
}, 100);
});
this.hooks.parallel.tapAsync('Task2', (data, callback) => {
setTimeout(() => {
console.log('Task2 done');
callback(null, 'result2');
}, 50);
});
}
call(data) {
this.hooks.parallel.callAsync(data, (err, results) => {
console.log('All tasks completed');
});
}
}
1.3.2 Webpack 构建流程
class SimpleWebpack {
constructor(options) {
this.options = options;
this.modules = {};
this.chunks = {};
this.assets = {};
}
run() {
const { entry, output, module, plugins } = this.options;
console.log('=== Webpack Build Start ===');
plugins.forEach(plugin => {
if (typeof plugin.apply === 'function') {
plugin.apply(this);
}
});
const entryModule = this.buildModule(entry, {});
const chunks = this.createChunks(entryModule);
this.emitAssets(chunks, output);
console.log('=== Webpack Build Complete ===');
}
buildModule(filename, parent) {
const source = fs.readFileSync(filename, 'utf-8');
console.log(`Building: ${filename}`);
const rules = this.options.module?.rules || [];
let processedSource = source;
for (const rule of rules) {
if (rule.test.test(filename)) {
const loader = rule.use;
if (typeof loader === 'string') {
const loaderFn = require(loader);
processedSource = loaderFn(processedSource);
}
}
}
const module = {
id: filename,
dependencies: [],
source: processedSource
};
const dependencyPattern = /require\(['"](.+?)['"]\)/g;
let match;
while ((match = dependencyPattern.exec(processedSource)) !== null) {
const depPath = path.resolve(dirname(filename), match[1]);
module.dependencies.push(depPath);
}
module.dependencies.forEach(dep => {
this.buildModule(dep, module);
});
return module;
}
createChunks(entryModule) {
return [{
id: 'main',
modules: [entryModule],
name: 'main'
}];
}
emitAssets(chunks, output) {
for (const chunk of chunks) {
const filename = output.filename.replace('[name]', chunk.name);
this.assets[filename] = this.generateCode(chunk);
const outputPath = path.join(output.path, filename);
fs.writeFileSync(outputPath, this.assets[filename]);
console.log(`Emitted: ${outputPath}`);
}
}
generateCode(chunk) {
return `
(function(modules) {
// 9.1 定义 require 函数
function __webpack_require__(moduleId) {
var module = {
exports: {}
};
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
return module.exports;
}
// 9.2 启动模块加载
return __webpack_require__("${chunk.modules[0].id}");
})({
"${chunk.modules[0].id}": function(module, exports, __webpack_require__) {
${chunk.modules[0].source}
}
})
`;
}
}
二、Vite 核心原理
2.1 Vite 核心配置
2.1.1 Vite 基础配置
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import react from '@vitejs/plugin-react';
import cssPlugin from 'vite-plugin-css';
import { viteStaticCopy } from 'vite-plugin-static-copy';
export default defineConfig({
root: process.cwd(),
base: '/',
server: {
port: 3000,
open: true,
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
},
cors: true,
host: true,
hmr: {
port: 3001,
overlay: true
}
},
build: {
target: 'es2015',
outDir: 'dist',
sourcemap: false,
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true
}
},
chunkSizeWarningLimit: 500,
rollupOptions: {
output: {
manualChunks: {
'vue-vendor': ['vue', 'vue-router', 'vuex'],
'element-ui': ['element-plus']
},
entryFileNames: 'js/[name]-[hash].js',
chunkFileNames: 'js/[name]-[hash].js',
assetFileNames: '[ext]/[name]-[hash][extname]'
}
}
},
plugins: [
vue({
include: [/\.vue$/],
customBlocks: {
'i18n': resolve(__dirname, 'src/i18n')
}
}),
react(),
viteStaticCopy({
targets: [
{ src: 'public/*', dest: 'public' }
]
})
],
resolve: {
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json'],
alias: {
'@': path.resolve(__dirname, 'src'),
'components': path.resolve(__dirname, 'src/components'),
'utils': path.resolve(__dirname, 'src/utils')
}
},
optimizeDeps: {
include: ['vue', 'vue-router', 'element-plus'],
exclude: [],
entries: ['src/main.js']
},
css: {
modules: {
localsConvention: 'camelCase',
exclude: []
},
preprocessorOptions: {
scss: {
additionalData: `@import "@/styles/variables.scss";`
}
}
},
logLevel: 'info',
envDir: path.resolve(__dirname, 'env'),
envPrefix: 'VITE_'
});
2.2 Vite 核心原理详解
2.2.1 Vite 工作流程
class ViteDevServer {
constructor() {
this.moduleCache = new Map();
this.transformCache = new Map();
this.depGraph = new Map();
}
async start() {
const server = http.createServer(async (req, res) => {
const url = req.url.split('?')[0];
const filePath = path.join(process.cwd(), url);
try {
let source = fs.readFileSync(filePath, 'utf-8');
if (filePath.endsWith('.js') || filePath.endsWith('.ts')) {
source = await this.transform(source, filePath);
}
res.setHeader('Content-Type', this.getContentType(filePath));
res.end(source);
} catch (err) {
if (err.code === 'ENOENT') {
res.statusCode = 404;
res.end('Not Found');
}
}
});
server.listen(3000);
}
async transform(source, filePath) {
if (this.transformCache.has(filePath)) {
return this.transformCache.get(filePath);
}
const result = await esbuild.transform(source, {
target: 'esnext',
sourcemap: true,
jsx: 'automatic',
format: 'esm'
});
const deps = this.extractDeps(source);
this.depGraph.set(filePath, {
imports: deps,
transformed: result.code
});
this.transformCache.set(filePath, result.code);
return result.code;
}
extractDeps(source) {
const deps = [];
const importRegex = /import\s+(?:(?:[\w*{}\s]+ from )|(?:.* from )|(?:[\w*{}\s]+,\s*\{[\s\w*}]+ from ))?['"]([@\w/-]+)['"]/g;
let match;
while ((match = importRegex.exec(source)) !== null) {
deps.push(match[1]);
}
return deps;
}
getContentType(filePath) {
const ext = path.extname(filePath);
const types = {
'.js': 'application/javascript',
'.ts': 'application/typescript',
'.css': 'text/css',
'.html': 'text/html',
'.json': 'application/json'
};
return types[ext] || 'text/plain';
}
}
2.2.2 Vite 热更新原理
const HMR_UPDATE_TYPES = {
'JS_UPDATE': 'js-update',
'CSS_UPDATE': 'css-update',
'RELOAD': 'reload'
};
function setupHMRClient() {
const socket = new WebSocket('ws://localhost:3001');
socket.addEventListener('message', async (event) => {
const { type, path, timestamp } = JSON.parse(event.data);
switch (type) {
case 'js-update':
await handleJSUpdate(path);
break;
case 'css-update':
await handleCSSUpdate(path);
break;
case 'reload':
window.location.reload();
break;
}
});
async function handleJSUpdate(path) {
const newModule = await fetchModule(path);
updateModuleCache(path, newModule);
const affectedModules = findAffectedModules(path);
for (const modulePath of affectedModules) {
const module = getModule(modulePath);
if (module && module.hot) {
if (typeof module.hot.accept === 'function') {
module.hot.accept();
}
}
}
console.log(`[HMR] Updated: ${path}`);
}
async function handleCSSUpdate(path) {
const newCSS = await fetchModule(path);
const style = document.querySelector(`link[href*="${path}"]`);
if (style) {
style.textContent = newCSS;
}
console.log(`[HMR] CSS Updated: ${path}`);
}
}
const vuePlugin = {
handleHotUpdate({ server, file, modules }) {
if (file.endsWith('.vue')) {
const descriptor = parse(file);
if (descriptor.template) {
server.ws.send({
type: 'css-update',
path: file
});
modules.forEach(m => {
server.reload(m);
});
}
if (descriptor.script) {
server.ws.send({
type: 'reload',
path: file
});
}
}
}
};
2.3 Vite 与 Webpack 对比
const compareConfig = {
webpack: {
mode: 'development',
entry: './src/main.js',
output: { filename: '[name].js' },
module: {
rules: [
{ test: /\.js$/, use: 'babel-loader' },
{ test: /\.vue$/, use: 'vue-loader' }
]
},
plugins: [
new HtmlWebpackPlugin(),
new VueLoaderPlugin()
],
resolve: {
alias: { '@': path.resolve(__dirname, 'src') }
}
},
vite: {
plugins: [vue()],
resolve: {
alias: { '@': path.resolve(__dirname, 'src') }
}
}
};
const适用场景 = {
vite: [
'小型/中型项目(< 500 个模块)',
'需要快速启动的开发环境',
'Vue 3 / React 17+ 项目',
'TypeScript 项目(esbuild 原生支持)'
],
webpack: [
'大型复杂项目(> 1000 个模块)',
'需要高度自定义的构建流程',
'旧项目迁移',
'需要兼容性优先(IE11)'
]
};
三、前端性能优化
3.1 运行时性能优化
3.1.1 React 性能优化
import { memo } from 'react';
const Button = memo(function Button({ onClick, children }) {
return <button onClick={onClick}>{children}</button>;
});
const ProductList = memo(
function ProductList({ products, onSelect }) {
return (
<ul>
{products.map(p => (
<ProductItem
key={p.id}
product={p}
onSelect={onSelect}
/>
))}
</ul>
);
},
(prevProps, nextProps) => {
return prevProps.products === nextProps.products;
}
);
import { useMemo } from 'react';
function ExpensiveList({ items, filter }) {
const filteredItems = useMemo(() => {
console.log('Filtering items...');
return items.filter(item => {
return item.name.includes(filter);
});
}, [items, filter]);
return <List data={filteredItems} />;
}
const contextValue = useMemo(() => ({
theme: 'dark',
toggleTheme: () => {}
}), []);
<ThemeContext.Provider value={contextValue}>
{children}
</ThemeContext.Provider>;
import { useCallback } from 'react';
function ParentComponent() {
const [count, setCount] = useState(0);
const handleClick = useCallback((event) => {
console.log('Clicked:', event.target);
setCount(c => c + 1);
}, []);
return <MemoizedButton onClick={handleClick} />;
}
const MemoizedButton = memo(function Button({ onClick, children }) {
console.log('Button rendered');
return <button onClick={onClick}>{children}</button>;
});
import { useState, useTransition } from 'react';
function SearchComponent() {
const [query, setQuery] = useState('');
const [isPending, startTransition] = useTransition();
const handleChange = (e) => {
const value = e.target.value;
setQuery(value);
startTransition(() => {
fetchResults(value);
});
};
return (
<div>
<input value={query} onChange={handleChange} />
{isPending && <LoadingSpinner />}
</div>
);
}
import { useDeferredValue } from 'react';
function SearchResults({ query }) {
const deferredQuery = useDeferredValue(query);
const isStale = query !== deferredQuery;
return (
<div style={{ opacity: isStale ? 0.5 : 1 }}>
{/* 根据 deferredQuery 渲染结果 */}
<Results query={deferredQuery} />
</div>
);
}
function Timer() {
const intervalRef = useRef(null);
const countRef = useRef(0);
useEffect(() => {
intervalRef.current = setInterval(() => {
countRef.current += 1;
console.log('Count:', countRef.current);
}, 1000);
return () => clearInterval(intervalRef.current);
}, []);
return <div>{countRef.current}</div>;
}
import { useVirtualizer } from 'react-virtual';
function VirtualList({ items }) {
const parentRef = useRef(null);
const rowVirtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50,
overscan: 5
});
return (
<div ref={parentRef} style={{ height: '400px', overflow: 'auto' }}>
<div style={{ height: rowVirtualizer.getTotalSize() }}>
{rowVirtualizer.getVirtualItems().map((virtualRow) => (
<div
key={virtualRow.key}
style={{
position: 'absolute',
top: virtualRow.start,
height: virtualRow.size
}}
>
<ListItem item={items[virtualRow.index]} />
</div>
))}
</div>
</div>
);
}
3.1.2 渲染性能优化
const BadList = ({ items }) => (
<ul>
{items.map((item, index) => (
// index 作为 key,删除 item[1] 后,item[2] 的 key 从 2 变成 1
// React 会认为这是同一个元素,只是位置变了
<li key={index}>{item.name}</li>
))}
</ul>
);
const GoodList = ({ items }) => (
<ul>
{items.map(item => (
// 唯一 id 作为 key,删除 item[1] 时,item[2] 不受影响
<li key={item.id}>{item.name}</li>
))}
</ul>
);
function BadComponent({ items }) {
return (
<ul>
{items.map(item => (
// onClick 每次都是新函数,导致子组件重渲染
<Item key={item.id} onClick={() => console.log(item.id)} />
))}
</ul>
);
}
const Item = memo(function Item({ id, onClick }) {
return <li onClick={onClick}>{id}</li>;
});
function TableRow() {
return (
<>
<td>Name</td>
<td>Age</td>
</>
);
}
function ConditionalRender({ shouldShow, heavyComponent }) {
if (!shouldShow) {
return null;
}
const LazyHeavy = lazy(() => import('./HeavyComponent'));
return (
<>
<Header />
{/* 懒加载,只有在需要时才加载 */}
{shouldShow && <LazyHeavy />}
</>
);
}
3.2 网络性能优化
3.2.1 资源加载优化
<link rel="preload" href="/fonts/custom.woff2" as="font" crossorigin="anonymous">
<link rel="preload" href="/main.js" as="script">
// 预加载 CSS
<link rel="preload" href="/styles.css" as="style">
// 2. 预连接
// 提前建立 TCP/TLS 连接
<link rel="preconnect" href="https://cdn.example.com">
<link rel="dns-prefetch" href="https://cdn.example.com">
// 3. 懒加载图片
// <img loading="lazy"> 原生懒加载
const LazyImage = ({ src, alt }) => (
<img loading="lazy" src={src} alt={alt} />
);
// 4. 第三方资源优化
// 使用 Intersection Observer 实现懒加载
class LazyLoad {
constructor(selector) {
// 4.1 创建 IntersectionObserver
// 当元素进入视口时触发回调
this.observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
// 4.2 加载资源
this.loadResource(entry.target);
// 4.3 停止观察
this.observer.unobserve(entry.target);
}
});
},
{
rootMargin: '50px 0px', // 提前 50px 开始加载
threshold: 0.01
}
);
// 4.4 开始观察所有目标元素
document.querySelectorAll(selector).forEach(el => {
this.observer.observe(el);
});
}
loadResource(element) {
// 处理图片懒加载
if (element.tagName === 'IMG') {
element.src = element.dataset.src;
}
// 处理脚本懒加载
if (element.tagName === 'SCRIPT') {
const script = document.createElement('script');
script.src = element.dataset.src;
document.body.appendChild(script);
}
}
}
// 使用
new LazyLoad('img[data-src], script[data-src]');
// 5. 请求合并
// 将多个小请求合并为一个大请求
class RequestBatcher {
constructor(batchSize = 10, delay = 100) {
this.queue = []; // 待处理请求队列
this.batchSize = batchSize; // 每批数量
this.delay = delay; // 延迟时间(ms)
// 定时器
this.timer = null;
}
// 5.1 添加请求
add(request) {
return new Promise((resolve, reject) => {
this.queue.push({ request, resolve, reject });
this.schedule();
});
}
// 5.2 调度执行
schedule() {
if (this.timer) return;
this.timer = setTimeout(() => {
this.flush();
this.timer = null;
}, this.delay);
}
// 5.3 执行批量请求
async flush() {
// 取出最多 batchSize 个请求
const batch = this.queue.splice(0, this.batchSize);
// 批量执行
try {
const results = await batchRequest(batch.map(b => b.request));
batch.forEach((item, index) => {
item.resolve(results[index]);
});
} catch (err) {
batch.forEach(item => item.reject(err));
}
}
}
// 6. DNS 预解析
// 提前解析域名,减少连接时间
<meta http-equiv="x-dns-prefetch-control" content="on">
<link rel="dns-prefetch" href="//api.example.com">
<link rel="dns-prefetch" href="//cdn.example.com">
3.2.2 缓存策略实现
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(registration => {
console.log('SW registered:', registration.scope);
});
}
const CACHE_NAME = 'v1';
const STATIC_ASSETS = [
'/',
'/index.html',
'/main.js',
'/styles.css'
];
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => {
return cache.addAll(STATIC_ASSETS);
})
);
self.skipWaiting();
});
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames
.filter(name => name !== CACHE_NAME)
.map(name => caches.delete(name))
);
})
);
self.clients.claim();
});
self.addEventListener('fetch', (event) => {
const { request } = event;
const url = new URL(request.url);
if (url.pathname.startsWith('/api/')) {
event.respondWith(networkFirst(request));
} else if (isStaticAsset(url.pathname)) {
event.respondWith(cacheFirst(request));
}
});
async function cacheFirst(request) {
const cached = await caches.match(request);
if (cached) {
return cached;
}
const response = await fetch(request);
const cache = await caches.open(CACHE_NAME);
cache.put(request, response.clone());
return response;
}
async function networkFirst(request) {
try {
const response = await fetch(request);
const cache = await caches.open(CACHE_NAME);
cache.put(request, response.clone());
return response;
} catch (error) {
const cached = await caches.match(request);
if (cached) {
return cached;
}
return new Response('Offline', { status: 503 });
}
}
const express = require('express');
const app = express();
app.use('/static/', express.static('public', {
maxAge: 31536000,
etag: true
}));
app.get('/api/data', (req, res) => {
const lastModified = req.headers['if-modified-since'];
const etag = req.headers['if-none-match'];
const data = getData();
if (lastModified === data.lastModified) {
return res.status(304).end();
}
if (etag === data.etag) {
return res.status(304).end();
}
res.set({
'Last-Modified': data.lastModified,
'ETag': data.etag,
'Cache-Control': 'private, max-age=60'
});
res.json(data);
});
class CacheManager {
constructor(storage = localStorage) {
this.storage = storage;
this.prefix = 'cache:';
}
set(key, value, ttl = 3600000) {
const data = {
value,
expiry: Date.now() + ttl
};
this.storage.setItem(this.prefix + key, JSON.stringify(data));
}
get(key) {
const raw = this.storage.getItem(this.prefix + key);
if (!raw) return null;
const data = JSON.parse(raw);
if (Date.now() > data.expiry) {
this.delete(key);
return null;
}
return data.value;
}
delete(key) {
this.storage.removeItem(this.prefix + key);
}
clear() {
Object.keys(this.storage)
.filter(key => key.startsWith(this.prefix))
.forEach(key => this.storage.removeItem(key));
}
}
3.3 构建性能优化
3.3.1 Webpack 构建优化
module.exports = {
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
include: path.resolve(__dirname, 'src'),
use: 'babel-loader'
}
]
},
cache: {
type: 'filesystem',
buildPath: '.cache',
cacheDirectory: path.resolve(__dirname, '.cache/babel'),
compression: 'gzip',
version: 'v1'
},
optimization: {
module: {
rules: [
{
test: /\.js$/,
use: [
{
loader: 'thread-loader',
options: {
workers: 4,
workerParallelJobs: 50
}
},
'babel-loader'
]
}
]
},
minimizer: [
new TerserPlugin({
parallel: true,
terserOptions: {
compress: {
drop_console: true
}
}
})
]
},
resolve: {
modules: [
path.resolve(__dirname, 'src'),
'node_modules'
],
extensions: ['.js', '.vue', '.json'],
alias: {
'@': path.resolve(__dirname, 'src'),
'vue$': 'vue/dist/vue.esm.js'
}
},
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 10,
reuseExistingChunk: true
},
common: {
minChunks: 2,
priority: 5,
reuseExistingChunk: true
}
}
},
runtimeChunk: 'single'
},
mode: 'production',
devtool: false,
optimization: {
usedExports: true,
sideEffects: true
},
module: {
rules: [
{
test: /\.css$/,
sideEffects: true
}
]
}
};
3.3.2 Vite 构建优化
import { defineConfig } from 'vite';
export default defineConfig({
optimizeDeps: {
include: [
'vue',
'vue-router',
'vuex',
'element-plus',
'lodash-es'
],
exclude: [],
buildId: 'vite',
clean: false
},
build: {
target: 'es2015',
sourcemap: false,
minify: 'terser',
chunkSizeWarningLimit: 500,
rollupOptions: {
output: {
manualChunks: {
'vue-vendor': ['vue', 'vue-router', 'vuex'],
'element-vendor': ['element-plus'],
'utils-vendor': ['lodash-es', 'axios']
},
vendorChunkId: 'vendors'
}
},
cssCodeSplit: true,
assetsInlineLimit: 4096
},
compress: {
gzip: true,
gzipSize: 0.5,
brotli: true
},
plugins: [
],
define: {
__APP_VERSION__: JSON.stringify(process.env.npm_package_version)
}
});
四、浏览器原理与前端架构
4.1 浏览器渲染原理
4.1.1 关键渲染路径
<script defer src="/main.js"></script>
<script async src="/analytics.js"></script>
<link rel="preload" href="/critical.css" as="style" onload="this.rel='stylesheet'">
// 5. 内联关键 CSS
// 将首屏需要的 CSS 内联到 <head> 中
<style>
/* 关键 CSS */
body { margin: 0; padding: 0; }
.header { background: #333; }
</style>
// 非关键 CSS 异步加载
<link rel="preload" href="/non-critical.css" as="style" onload="this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/non-critical.css"></noscript>
4.1.2 事件循环与渲染调度
function foo() {
bar();
}
function bar() {
console.log('bar');
}
foo();
console.log('1');
setTimeout(() => {
console.log('2');
}, 0);
Promise.resolve().then(() => {
console.log('3');
});
console.log('4');
let startTime;
function animate(timestamp) {
if (!startTime) startTime = timestamp;
const elapsed = timestamp - startTime;
element.style.transform = `translateX(${elapsed}px)`;
if (elapsed < 1000) {
requestAnimationFrame(animate);
}
}
requestAnimationFrame(animate);
requestIdleCallback((deadline) => {
while (deadline.timeRemaining() > 0 && tasks.length > 0) {
const task = tasks.shift();
task();
}
if (tasks.length > 0) {
requestIdleCallback(arguments.callee);
}
}, { timeout: 2000 });
console.log('script start');
setTimeout(() => console.log('setTimeout'), 0);
Promise.resolve()
.then(() => console.log('Promise 1'))
.then(() => console.log('Promise 2'));
queueMicrotask(() => console.log('queueMicrotask'));
console.log('script end');
element.style.transform = 'translateX(100px)';
element.style.transform = 'translateX(200px)';
element.style.transform = 'translateX(200px)';
class BatchUpdater {
constructor() {
this.pendingUpdates = [];
this.frameId = null;
}
scheduleUpdate(update) {
this.pendingUpdates.push(update);
if (!this.frameId) {
this.frameId = requestAnimationFrame(() => {
this.flush();
});
}
}
flush() {
this.pendingUpdates.forEach(fn => fn());
this.pendingUpdates = [];
this.frameId = null;
}
}
4.2 前端架构设计
4.2.1 微前端架构
class MicroFrontend {
constructor() {
this.apps = {};
this.currentApp = null;
}
register(name, entry, props = {}) {
this.apps[name] = {
entry,
props,
instance: null,
activeRule: null
};
}
setActiveRule(name, rule) {
if (this.apps[name]) {
this.apps[name].activeRule = rule;
}
}
async mount(name) {
const app = this.apps[name];
if (!app) throw new Error(`App ${name} not found`);
const { scripts, styles } = await this.loadAssets(app.entry);
await this.loadStyles(styles);
const mountFunction = await this.loadScripts(scripts);
app.instance = mountFunction(app.props);
this.currentApp = name;
}
async unmount(name) {
const app = this.apps[name];
if (app && app.instance) {
if (typeof app.instance.unmount === 'function') {
await app.instance.unmount();
}
app.instance = null;
}
if (this.currentApp === name) {
this.currentApp = null;
}
}
async loadAssets(entry) {
const html = await fetch(entry).then(r => r.text());
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const scripts = Array.from(doc.querySelectorAll('script[src]'))
.map(s => s.src);
const styles = Array.from(doc.querySelectorAll('link[href]'))
.filter(l => l.rel === 'stylesheet')
.map(l => l.href);
return { scripts, styles };
}
async loadStyles(urls) {
await Promise.all(urls.map(url => {
return new Promise((resolve, reject) => {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = url;
link.onload = resolve;
link.onerror = reject;
document.head.appendChild(link);
});
}));
}
async loadScripts(urls) {
for (const url of urls) {
await this.loadScript(url);
}
}
async loadScript(url) {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = url;
script.onload = resolve;
script.onerror = reject;
document.body.appendChild(script);
});
}
}
export function mount(props) {
const app = createApp(App);
app.mount(props.container);
return {
unmount() {
app.unmount();
}
};
}
function setupRouter() {
const routes = [
{ path: '/app1/*', app: 'app1' },
{ path: '/app2/*', app: 'app2' }
];
window.addEventListener('popstate', () => {
renderRoute();
});
function renderRoute() {
const path = window.location.pathname;
for (const route of routes) {
if (path.match(route.path)) {
microFrontend.mount(route.app);
return;
}
}
microFrontend.unmount(currentApp);
}
document.addEventListener('click', (e) => {
const link = e.target.closest('a[href]');
if (link && link.href.startsWith(window.location.origin)) {
e.preventDefault();
window.history.pushState(null, '', link.href);
renderRoute();
}
});
renderRoute();
}
function emit(event, data) {
window.dispatchEvent(new CustomEvent(`mf:${event}`, { detail: data }));
}
window.addEventListener('mf:data', (e) => {
console.log('Received:', e.detail);
});
const sharedState = {
user: null,
setUser(user) {
this.user = user;
window.dispatchEvent(new CustomEvent('user:change', { detail: user }));
}
};
function getSharedState() {
return sharedState.user;
}
4.2.2 模块联邦(Module Federation)
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'host_app',
exposes: {
'./Navbar': './src/components/Navbar'
},
shared: {
vue: {
singleton: true,
requiredVersion: '^3.0.0'
},
vuex: {
singleton: true
}
}
})
]
};
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'remote_app',
exposes: {
'./Button': './src/components/Button',
'./Card': './src/components/Card'
},
filename: 'remoteEntry.js',
shared: ['vue']
})
]
};
import React, { lazy, Suspense } from 'react';
const Button = lazy(() => import('remote_app/Button'));
function App() {
return (
<div>
<h1>Host App</h1>
<Suspense fallback={<div>Loading Button...</div>}>
<Button />
</Suspense>
</div>
);
}
const container = document.createElement('div');
async function loadRemoteModule(remoteName, moduleName) {
await __webpack_require__.e(`${remoteName}-${moduleName}`);
return __webpack_require__(`.${remoteName}/${moduleName}`);
}
async function loadButton() {
const { default: Button } = await loadRemoteModule('remote_app', './Button');
return Button;
}
new ModuleFederationPlugin({
name: 'shared_lib',
exposes: {
'./types': './src/types/index.ts'
}
});
{
"compilerOptions": {
"paths": {
"shared_lib/*": ["./node_modules/shared_lib/*"]
}
}
}
new ModuleFederationPlugin({
name: 'remote_app',
exposes: {
'./store': './src/store'
},
shared: ['vue']
});
import("./remote_app/store").then(module => {
module.initStore(hostStore);
});
4.3 前端监控与错误处理
4.3.1 错误监控实现
class ErrorMonitor {
constructor(options = {}) {
this.options = options;
this.errors = [];
this.maxQueue = options.maxQueue || 100;
}
init() {
window.addEventListener('error', (event) => {
this.handleError({
type: 'error',
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
stack: event.error?.stack
});
});
window.addEventListener('unhandledrejection', (event) => {
this.handleError({
type: 'unhandledrejection',
message: event.reason?.message || event.reason,
stack: event.reason?.stack
});
});
window.addEventListener('error', (event) => {
if (event.target !== window) {
this.handleError({
type: 'resource',
message: `Failed to load: ${event.target.src || event.target.href}`
});
}
}, true);
}
handleError(error) {
const errorInfo = {
...error,
timestamp: Date.now(),
url: window.location.href,
userAgent: navigator.userAgent
};
this.errors.push(errorInfo);
if (this.errors.length > this.maxQueue) {
this.errors.shift();
}
this.report(errorInfo);
}
report(errorInfo) {
setTimeout(() => {
this.sendToServer(errorInfo);
}, 1000);
}
async sendToServer(errorInfo) {
try {
await fetch('/api/errors', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(errorInfo)
});
} catch (e) {
console.error('Failed to report error:', e);
}
}
getErrors() {
return [...this.errors];
}
clear() {
this.errors = [];
}
}
const monitor = new ErrorMonitor({ maxQueue: 50 });
monitor.init();
Vue.config.errorHandler = (err, vm, info) => {
console.error('Vue Error:', err);
console.error('Component:', vm);
console.error('Info:', info);
};
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
console.error('React Error:', error, errorInfo);
reportError(error, errorInfo);
}
render() {
if (this.state.hasError) {
return this.props.fallback || <div>出错了</div>;
}
return this.props.children;
}
}
<ErrorBoundary fallback={<div>加载失败</div>}>
<MyComponent />
</ErrorBoundary>;
class PerformanceMonitor {
constructor() {
this.metrics = {};
}
observeLoad() {
new PerformanceObserver((entryList) => {
const entries = entryList.getEntries();
const lastEntry = entries[entries.length - 1];
console.log('LCP:', lastEntry.startTime);
}).observe({ entryTypes: ['largest-contentful-paint'] });
new PerformanceObserver((entryList) => {
const entries = entryList.getEntries();
entries.forEach(entry => {
console.log('FID:', entry.processingStart - entry.startTime);
});
}).observe({ entryTypes: ['first-input'] });
new PerformanceObserver((entryList) => {
let cls = 0;
entries.forEach(entry => {
if (!entry.hadRecentInput) {
cls += entry.value;
}
});
console.log('CLS:', cls);
}).observe({ entryTypes: ['layout-shift'] });
}
observeResource() {
new PerformanceObserver((entryList) => {
entries.getEntries().forEach(entry => {
console.log('Resource:', {
name: entry.name,
duration: entry.duration,
size: entry.transferSize
});
});
}).observe({ entryTypes: ['resource'] });
}
getMetrics() {
const timing = performance.timing;
return {
dns: timing.domainLookupEnd - timing.domainLookupStart,
tcp: timing.connectEnd - timing.connectStart,
ttfb: timing.responseStart - timing.requestStart,
domParse: timing.domInteractive - timing.responseEnd,
domReady: timing.domContentLoadedEventEnd - timing.navigationStart,
fcp: performance.getEntriesByType('paint')[0]?.startTime,
lcp: this.getLCP()
};
}
getLCP() {
const entries = performance.getEntriesByType('largest-contentful-paint');
return entries[entries.length - 1]?.startTime;
}
}
五、综合架构设计
5.1 前端工程化体系
const projectStructure = {
'src': {
'pages': {
'Home': {
'index.js',
'index.scss',
'components': {}
}
},
'components': {
'Button': {
'index.js',
'index.scss',
'index.test.js'
}
},
'utils': {
'request.js',
'storage.js',
'format.js'
},
'styles': {
'variables.scss',
'mixins.scss',
'reset.scss'
},
'services': {
'user.js',
'product.js'
},
'store': {
'index.js',
'user.js',
'cart.js'
},
'router': {
'index.js',
'routes.js'
},
'config': {
'index.js',
'dev.js',
'prod.js'
}
},
'config': {
'jest.config.js',
'eslintrc.js',
'prettierrc.js',
'commitlint.config.js'
},
'scripts': {
'build.js',
'analyze.js'
},
'docs': {}
};
const packageScripts = {
"dev": "vite --mode development",
"dev:prod": "vite --mode production",
"build": "vite build",
"build:analyze": "vite build --mode analyze",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"lint": "eslint src --ext .js,.vue,.jsx,.tsx",
"lint:fix": "eslint src --fix",
"lint:style": "stylelint src/**/*.{css,scss,vue}",
"format": "prettier --write \"src/**/*.{js,vue,jsx,tsx,css,scss}\"",
"commit": "cz",
"push": "git push",
"pre-commit": "npm run lint && npm run test",
"ci": "npm install && npm run lint && npm run test && npm run build"
};
const viteEnvConfig = {
'VITE_APP_TITLE': 'My App (Dev)',
'VITE_API_BASE_URL': 'http://localhost:3000',
'VITE_ENABLE_MOCK': 'true',
'VITE_APP_TITLE': 'My App',
'VITE_API_BASE_URL': 'https://api.example.com',
'VITE_ENABLE_MOCK': 'false'
};
const ciConfig = `
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '16'
- run: npm ci
- run: npm run lint
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '16'
- run: npm ci
- run: npm run test:coverage
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '16'
- run: npm ci
- run: npm run build
env:
VITE_API_BASE_URL: \${{ secrets.API_URL }}
`;
const pnpmWorkspace = `
packages:
- 'packages/*'
`;
new ModuleFederationPlugin({
name: 'host',
remotes: {
remote_app: 'remote_app@http://localhost:3001/remoteEntry.js'
},
shared: ['vue']
});
六、学习清单
| 类别 | 题目 | 难度 | 重点 |
|---|
| Webpack | loader 和 plugin 区别 | ⭐ | 生命周期、执行顺序 |
| Webpack | Webpack 构建流程 | ⭐⭐ | Tapable、编译流程 |
| Webpack | splitChunks 原理 | ⭐⭐ | 分包策略、缓存 |
| Webpack | Tree-shaking 原理 | ⭐⭐ | sideEffects、usedExports |
| Webpack | 手写 loader | ⭐⭐ | 同步/异步、pitch |
| Vite | Vite 工作原理 | ⭐⭐ | ESM、按需编译 |
| Vite | Vite HMR 原理 | ⭐⭐ | WebSocket、模块更新 |
| Vite | Vite 与 Webpack 对比 | ⭐ | 启动速度、构建速度 |
| React | useMemo/useCallback 区别 | ⭐ | 依赖、缓存策略 |
| React | React.memo 原理 | ⭐ | 浅比较、自定义比较 |
| React | useTransition 作用 | ⭐⭐ | 并发模式、优先级 |
| React | 虚拟列表原理 | ⭐⭐ | 懒渲染、滚动计算 |
| 性能 | 关键渲染路径优化 | ⭐⭐ | CRP、阻塞分析 |
| 性能 | 防抖/节流原理 | ⭐ | 场景、区别 |
| 性能 | 浏览器缓存策略 | ⭐⭐ | 强缓存、协商缓存 |
| 架构 | 微前端实现 | ⭐⭐⭐ | qiankun、single-spa |
| 架构 | 模块联邦原理 | ⭐⭐ | 共享依赖、远程加载 |
| 监控 | 错误边界原理 | ⭐⭐ | 生命周期、错误恢复 |