10.1 性能优化概述
10.1.1 性能指标
在 Tailwind CSS 项目中,我们需要关注以下性能指标:
- CSS 文件大小:最终生成的 CSS 文件大小
- 构建时间:从源码到生产文件的编译时间
- 运行时性能:浏览器渲染和交互性能
- 首次内容绘制 (FCP):页面首次渲染内容的时间
- 最大内容绘制 (LCP):页面主要内容完成渲染的时间
- 累积布局偏移 (CLS):页面布局稳定性指标
10.1.2 优化策略
- 减少 CSS 文件大小
- 优化构建流程
- 提升运行时性能
- 改善用户体验
- 监控和分析
10.2 CSS 文件大小优化
10.2.1 内容配置优化
// tailwind.config.js - 精确的内容配置
module.exports = {
content: [
// 只包含实际使用的文件
'./src/**/*.{html,js,ts,jsx,tsx}',
'./components/**/*.{js,ts,jsx,tsx}',
'./pages/**/*.{js,ts,jsx,tsx}',
// 避免包含不必要的文件
// './node_modules/**/*.js', // 避免这样做
// 使用更精确的模式
'./src/components/**/*.vue',
'./src/pages/**/*.vue',
],
// 安全列表 - 只添加确实需要的类
safelist: [
// 动态生成的类名
{
pattern: /bg-(red|green|blue)-(100|500|900)/,
variants: ['hover', 'focus'],
},
// 第三方库需要的类
'prose',
'prose-lg',
],
// 阻止列表 - 移除不需要的类
blocklist: [
'container', // 如果不使用容器类
'debug-screens', // 调试相关的类
],
}
10.2.2 按需加载策略
/* 基础样式文件 - base.css */
@tailwind base;
/* 组件样式文件 - components.css */
@tailwind components;
/* 工具类文件 - utilities.css */
@tailwind utilities;
/* 分离关键 CSS */
/* critical.css - 首屏必需的样式 */
@layer base {
body {
@apply font-sans text-gray-900 bg-white;
}
}
@layer components {
.btn {
@apply px-4 py-2 rounded font-medium transition-colors;
}
.btn-primary {
@apply bg-blue-500 text-white hover:bg-blue-600;
}
}
/* non-critical.css - 非关键样式 */
@layer utilities {
.text-shadow {
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
}
10.2.3 CSS 压缩和优化
// postcss.config.js
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
// 生产环境优化
...(process.env.NODE_ENV === 'production' && {
// CSS 压缩
cssnano: {
preset: ['default', {
discardComments: {
removeAll: true,
},
normalizeWhitespace: true,
colormin: true,
convertValues: true,
discardDuplicates: true,
discardEmpty: true,
mergeRules: true,
minifyFontValues: true,
minifyParams: true,
minifySelectors: true,
normalizeCharset: true,
normalizeDisplayValues: true,
normalizePositions: true,
normalizeRepeatStyle: true,
normalizeString: true,
normalizeTimingFunctions: true,
normalizeUnicode: true,
normalizeUrl: true,
orderedValues: true,
reduceIdents: true,
reduceInitial: true,
reduceTransforms: true,
svgo: true,
uniqueSelectors: true,
}],
},
// 移除未使用的 CSS
'@fullhuman/postcss-purgecss': {
content: [
'./src/**/*.html',
'./src/**/*.js',
'./src/**/*.jsx',
'./src/**/*.ts',
'./src/**/*.tsx',
],
defaultExtractor: content => content.match(/[\w-/:]+(?<!:)/g) || [],
safelist: {
standard: ['html', 'body'],
deep: [/^prose/, /^hljs/],
greedy: [/^bg-/, /^text-/],
},
},
}),
},
}
10.2.4 文件大小分析
// analyze-css.js - CSS 文件分析脚本
const fs = require('fs')
const path = require('path')
const gzipSize = require('gzip-size')
const brotliSize = require('brotli-size')
async function analyzeCSSFile(filePath) {
const content = fs.readFileSync(filePath, 'utf8')
const stats = fs.statSync(filePath)
const originalSize = stats.size
const gzipped = await gzipSize(content)
const brotli = await brotliSize(content)
console.log(`\n📊 CSS 文件分析: ${path.basename(filePath)}`)
console.log(`原始大小: ${(originalSize / 1024).toFixed(2)} KB`)
console.log(`Gzip 压缩: ${(gzipped / 1024).toFixed(2)} KB (${((1 - gzipped / originalSize) * 100).toFixed(1)}% 减少)`)
console.log(`Brotli 压缩: ${(brotli / 1024).toFixed(2)} KB (${((1 - brotli / originalSize) * 100).toFixed(1)}% 减少)`)
// 性能建议
if (originalSize > 100 * 1024) {
console.log('⚠️ 警告: CSS 文件过大,建议进行优化')
}
if (gzipped > 50 * 1024) {
console.log('💡 建议: 考虑代码分割或按需加载')
}
return {
original: originalSize,
gzipped,
brotli,
}
}
// 使用示例
analyzeCSSFile('./dist/styles.css')
10.3 构建性能优化
10.3.1 JIT 模式优化
// tailwind.config.js - JIT 模式配置
module.exports = {
mode: 'jit', // 启用 JIT 模式(Tailwind CSS 3.0+ 默认启用)
content: [
'./src/**/*.{html,js,ts,jsx,tsx}',
],
theme: {
extend: {
// 只扩展需要的配置
colors: {
primary: '#3b82f6',
},
},
},
// 禁用不需要的核心插件
corePlugins: {
float: false,
clear: false,
skew: false,
caretColor: false,
sepia: false,
},
}
10.3.2 并行构建
// webpack.config.js - Webpack 并行构建
const path = require('path')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
const TerserPlugin = require('terser-webpack-plugin')
module.exports = {
mode: 'production',
entry: {
main: './src/index.js',
styles: './src/styles.css',
},
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
importLoaders: 1,
},
},
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [
require('tailwindcss'),
require('autoprefixer'),
],
},
},
},
],
},
],
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].[contenthash].css',
}),
],
optimization: {
minimizer: [
new TerserPlugin({
parallel: true, // 并行压缩
}),
new OptimizeCSSAssetsPlugin({
cssProcessorOptions: {
map: {
inline: false,
annotation: true,
},
},
}),
],
splitChunks: {
cacheGroups: {
styles: {
name: 'styles',
test: /\.css$/,
chunks: 'all',
enforce: true,
},
},
},
},
}
10.3.3 缓存策略
// build-cache.js - 构建缓存策略
const fs = require('fs')
const path = require('path')
const crypto = require('crypto')
class BuildCache {
constructor(cacheDir = '.cache') {
this.cacheDir = cacheDir
this.ensureCacheDir()
}
ensureCacheDir() {
if (!fs.existsSync(this.cacheDir)) {
fs.mkdirSync(this.cacheDir, { recursive: true })
}
}
getFileHash(filePath) {
const content = fs.readFileSync(filePath)
return crypto.createHash('md5').update(content).digest('hex')
}
getCacheKey(files) {
const hashes = files.map(file => this.getFileHash(file))
return crypto.createHash('md5').update(hashes.join('')).digest('hex')
}
get(key) {
const cachePath = path.join(this.cacheDir, `${key}.json`)
if (fs.existsSync(cachePath)) {
return JSON.parse(fs.readFileSync(cachePath, 'utf8'))
}
return null
}
set(key, data) {
const cachePath = path.join(this.cacheDir, `${key}.json`)
fs.writeFileSync(cachePath, JSON.stringify(data, null, 2))
}
shouldRebuild(sourceFiles, outputFile) {
if (!fs.existsSync(outputFile)) {
return true
}
const outputStat = fs.statSync(outputFile)
for (const sourceFile of sourceFiles) {
if (fs.existsSync(sourceFile)) {
const sourceStat = fs.statSync(sourceFile)
if (sourceStat.mtime > outputStat.mtime) {
return true
}
}
}
return false
}
}
// 使用示例
const cache = new BuildCache()
const sourceFiles = ['./src/styles.css', './tailwind.config.js']
const outputFile = './dist/styles.css'
if (cache.shouldRebuild(sourceFiles, outputFile)) {
console.log('🔄 需要重新构建 CSS')
// 执行构建逻辑
} else {
console.log('✅ CSS 文件是最新的,跳过构建')
}
module.exports = BuildCache
10.3.4 增量构建
// incremental-build.js
const chokidar = require('chokidar')
const { execSync } = require('child_process')
const BuildCache = require('./build-cache')
class IncrementalBuilder {
constructor(options = {}) {
this.cache = new BuildCache()
this.watchPaths = options.watchPaths || ['./src/**/*']
this.buildCommand = options.buildCommand || 'npm run build-css'
this.debounceTime = options.debounceTime || 300
this.buildTimeout = null
}
start() {
console.log('🚀 启动增量构建监听...')
const watcher = chokidar.watch(this.watchPaths, {
ignored: /(^|[\/\\])\../, // 忽略隐藏文件
persistent: true,
})
watcher.on('change', (path) => {
console.log(`📝 文件变更: ${path}`)
this.scheduleBuild()
})
watcher.on('add', (path) => {
console.log(`➕ 新增文件: ${path}`)
this.scheduleBuild()
})
watcher.on('unlink', (path) => {
console.log(`🗑️ 删除文件: ${path}`)
this.scheduleBuild()
})
// 初始构建
this.build()
}
scheduleBuild() {
if (this.buildTimeout) {
clearTimeout(this.buildTimeout)
}
this.buildTimeout = setTimeout(() => {
this.build()
}, this.debounceTime)
}
build() {
const startTime = Date.now()
try {
console.log('🔨 开始构建...')
execSync(this.buildCommand, { stdio: 'inherit' })
const duration = Date.now() - startTime
console.log(`✅ 构建完成 (${duration}ms)`)
} catch (error) {
console.error('❌ 构建失败:', error.message)
}
}
}
// 使用示例
const builder = new IncrementalBuilder({
watchPaths: ['./src/**/*.{html,js,jsx,ts,tsx}', './tailwind.config.js'],
buildCommand: 'tailwindcss -i ./src/input.css -o ./dist/output.css',
debounceTime: 200,
})
builder.start()
10.4 运行时性能优化
10.4.1 CSS 加载优化
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>性能优化示例</title>
<!-- 关键 CSS 内联 -->
<style>
/* 首屏关键样式 */
body {
font-family: system-ui, -apple-system, sans-serif;
margin: 0;
padding: 0;
background-color: #ffffff;
color: #1f2937;
}
.hero {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.loading {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid rgba(255,255,255,.3);
border-radius: 50%;
border-top-color: #fff;
animation: spin 1s ease-in-out infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
</style>
<!-- 预加载关键资源 -->
<link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/css/critical.css" as="style">
<!-- 异步加载非关键 CSS -->
<link rel="preload" href="/css/styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/css/styles.css"></noscript>
<!-- DNS 预解析 -->
<link rel="dns-prefetch" href="//fonts.googleapis.com">
<link rel="dns-prefetch" href="//api.example.com">
</head>
<body>
<div class="hero">
<div class="text-center">
<h1 class="text-4xl font-bold mb-4">性能优化示例</h1>
<div class="loading"></div>
</div>
</div>
<!-- 延迟加载的内容 -->
<div id="content" style="display: none;">
<!-- 页面主要内容 -->
</div>
<script>
// 延迟加载非关键内容
window.addEventListener('load', function() {
setTimeout(function() {
document.getElementById('content').style.display = 'block';
}, 100);
});
// 异步加载 CSS 的 polyfill
(function() {
var links = document.querySelectorAll('link[rel="preload"][as="style"]');
for (var i = 0; i < links.length; i++) {
var link = links[i];
if (link.onload === null) {
link.rel = 'stylesheet';
}
}
})();
</script>
</body>
</html>
10.4.2 图片优化
<!-- 响应式图片优化 -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div class="bg-white rounded-lg shadow-md overflow-hidden">
<!-- 使用 picture 元素进行响应式图片 -->
<picture>
<!-- WebP 格式优先 -->
<source
srcset="/images/card-1-320.webp 320w,
/images/card-1-640.webp 640w,
/images/card-1-960.webp 960w"
sizes="(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw"
type="image/webp"
>
<!-- 回退到 JPEG -->
<img
src="/images/card-1-640.jpg"
srcset="/images/card-1-320.jpg 320w,
/images/card-1-640.jpg 640w,
/images/card-1-960.jpg 960w"
sizes="(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw"
alt="卡片图片"
class="w-full h-48 object-cover"
loading="lazy"
decoding="async"
>
</picture>
<div class="p-6">
<h3 class="text-lg font-semibold mb-2">卡片标题</h3>
<p class="text-gray-600">卡片描述内容...</p>
</div>
</div>
</div>
<!-- 懒加载图片组件 -->
<div class="lazy-image-container">
<img
data-src="/images/large-image.jpg"
data-srcset="/images/large-image-320.jpg 320w,
/images/large-image-640.jpg 640w,
/images/large-image-1280.jpg 1280w"
data-sizes="(max-width: 768px) 100vw, 50vw"
alt="大图片"
class="w-full h-auto opacity-0 transition-opacity duration-300 lazy"
>
<!-- 占位符 -->
<div class="absolute inset-0 bg-gray-200 animate-pulse flex items-center justify-center">
<svg class="w-8 h-8 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm12 12H4l4-8 3 6 2-4 3 6z" clip-rule="evenodd"></path>
</svg>
</div>
</div>
<script>
// 图片懒加载实现
class LazyImageLoader {
constructor() {
this.images = document.querySelectorAll('img.lazy');
this.imageObserver = null;
this.init();
}
init() {
if ('IntersectionObserver' in window) {
this.imageObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.loadImage(entry.target);
this.imageObserver.unobserve(entry.target);
}
});
}, {
rootMargin: '50px 0px',
threshold: 0.01
});
this.images.forEach(img => this.imageObserver.observe(img));
} else {
// 回退方案
this.images.forEach(img => this.loadImage(img));
}
}
loadImage(img) {
const src = img.dataset.src;
const srcset = img.dataset.srcset;
const sizes = img.dataset.sizes;
if (src) {
img.src = src;
}
if (srcset) {
img.srcset = srcset;
}
if (sizes) {
img.sizes = sizes;
}
img.onload = () => {
img.classList.remove('opacity-0');
img.classList.add('opacity-100');
// 隐藏占位符
const placeholder = img.nextElementSibling;
if (placeholder && placeholder.classList.contains('animate-pulse')) {
placeholder.style.display = 'none';
}
};
img.classList.remove('lazy');
}
}
// 初始化懒加载
new LazyImageLoader();
</script>
10.4.3 JavaScript 优化
// performance-utils.js - 性能工具函数
class PerformanceUtils {
// 防抖函数
static debounce(func, wait, immediate = false) {
let timeout;
return function executedFunction(...args) {
const later = () => {
timeout = null;
if (!immediate) func(...args);
};
const callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func(...args);
};
}
// 节流函数
static throttle(func, limit) {
let inThrottle;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
// 虚拟滚动
static createVirtualScroll(container, items, itemHeight, renderItem) {
const containerHeight = container.clientHeight;
const visibleCount = Math.ceil(containerHeight / itemHeight) + 2;
let scrollTop = 0;
let startIndex = 0;
const viewport = document.createElement('div');
viewport.style.height = `${items.length * itemHeight}px`;
viewport.style.position = 'relative';
const content = document.createElement('div');
content.style.position = 'absolute';
content.style.top = '0';
content.style.width = '100%';
viewport.appendChild(content);
container.appendChild(viewport);
const updateVisibleItems = () => {
startIndex = Math.floor(scrollTop / itemHeight);
const endIndex = Math.min(startIndex + visibleCount, items.length);
content.style.transform = `translateY(${startIndex * itemHeight}px)`;
content.innerHTML = '';
for (let i = startIndex; i < endIndex; i++) {
const item = renderItem(items[i], i);
item.style.height = `${itemHeight}px`;
content.appendChild(item);
}
};
container.addEventListener('scroll', this.throttle(() => {
scrollTop = container.scrollTop;
updateVisibleItems();
}, 16));
updateVisibleItems();
}
// 性能监控
static measurePerformance(name, fn) {
return async (...args) => {
const start = performance.now();
const result = await fn(...args);
const end = performance.now();
console.log(`⏱️ ${name}: ${(end - start).toFixed(2)}ms`);
// 发送到分析服务
if (window.gtag) {
window.gtag('event', 'timing_complete', {
name: name,
value: Math.round(end - start)
});
}
return result;
};
}
// 内存使用监控
static monitorMemory() {
if (performance.memory) {
const memory = performance.memory;
console.log('💾 内存使用情况:');
console.log(`已使用: ${(memory.usedJSHeapSize / 1048576).toFixed(2)} MB`);
console.log(`总计: ${(memory.totalJSHeapSize / 1048576).toFixed(2)} MB`);
console.log(`限制: ${(memory.jsHeapSizeLimit / 1048576).toFixed(2)} MB`);
}
}
// 检测性能问题
static detectPerformanceIssues() {
// 检测长任务
if ('PerformanceObserver' in window) {
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
if (entry.duration > 50) {
console.warn(`🐌 检测到长任务: ${entry.name} (${entry.duration.toFixed(2)}ms)`);
}
});
});
observer.observe({ entryTypes: ['longtask'] });
}
// 检测布局偏移
if ('PerformanceObserver' in window) {
const observer = new PerformanceObserver((list) => {
let clsValue = 0;
list.getEntries().forEach((entry) => {
if (!entry.hadRecentInput) {
clsValue += entry.value;
}
});
if (clsValue > 0.1) {
console.warn(`📐 检测到布局偏移: ${clsValue.toFixed(4)}`);
}
});
observer.observe({ entryTypes: ['layout-shift'] });
}
}
}
// 使用示例
const optimizedSearch = PerformanceUtils.debounce((query) => {
// 搜索逻辑
console.log('搜索:', query);
}, 300);
const optimizedScroll = PerformanceUtils.throttle(() => {
// 滚动处理逻辑
console.log('滚动事件');
}, 16);
// 性能监控
const monitoredFunction = PerformanceUtils.measurePerformance('数据加载', async () => {
// 异步数据加载
await new Promise(resolve => setTimeout(resolve, 1000));
return '数据';
});
// 启动性能监控
PerformanceUtils.detectPerformanceIssues();
setInterval(() => {
PerformanceUtils.monitorMemory();
}, 30000);
10.5 用户体验优化
10.5.1 加载状态优化
<!-- 加载状态组件 -->
<div class="loading-states-demo">
<!-- 骨架屏加载 -->
<div class="skeleton-loader mb-8">
<h3 class="text-lg font-semibold mb-4">骨架屏加载</h3>
<div class="animate-pulse">
<div class="flex space-x-4">
<div class="rounded-full bg-gray-300 h-12 w-12"></div>
<div class="flex-1 space-y-2 py-1">
<div class="h-4 bg-gray-300 rounded w-3/4"></div>
<div class="h-3 bg-gray-300 rounded w-1/2"></div>
</div>
</div>
<div class="mt-4 space-y-3">
<div class="h-3 bg-gray-300 rounded"></div>
<div class="h-3 bg-gray-300 rounded w-5/6"></div>
<div class="h-3 bg-gray-300 rounded w-4/6"></div>
</div>
</div>
</div>
<!-- 进度条加载 -->
<div class="progress-loader mb-8">
<h3 class="text-lg font-semibold mb-4">进度条加载</h3>
<div class="w-full bg-gray-200 rounded-full h-2">
<div class="bg-blue-500 h-2 rounded-full transition-all duration-300 ease-out"
style="width: 0%"
id="progress-bar"></div>
</div>
<p class="text-sm text-gray-600 mt-2" id="progress-text">加载中... 0%</p>
</div>
<!-- 旋转加载器 -->
<div class="spinner-loader mb-8">
<h3 class="text-lg font-semibold mb-4">旋转加载器</h3>
<div class="flex items-center space-x-4">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div>
<div class="flex space-x-1">
<div class="w-2 h-2 bg-blue-500 rounded-full animate-bounce"></div>
<div class="w-2 h-2 bg-blue-500 rounded-full animate-bounce" style="animation-delay: 0.1s"></div>
<div class="w-2 h-2 bg-blue-500 rounded-full animate-bounce" style="animation-delay: 0.2s"></div>
</div>
<div class="animate-pulse flex space-x-1">
<div class="w-3 h-3 bg-blue-500 rounded-full"></div>
<div class="w-3 h-3 bg-blue-500 rounded-full"></div>
<div class="w-3 h-3 bg-blue-500 rounded-full"></div>
</div>
</div>
</div>
</div>
<script>
// 进度条动画
function animateProgress() {
const progressBar = document.getElementById('progress-bar');
const progressText = document.getElementById('progress-text');
let progress = 0;
const interval = setInterval(() => {
progress += Math.random() * 15;
if (progress > 100) {
progress = 100;
clearInterval(interval);
}
progressBar.style.width = `${progress}%`;
progressText.textContent = `加载中... ${Math.round(progress)}%`;
if (progress === 100) {
progressText.textContent = '加载完成!';
}
}, 200);
}
// 启动进度条动画
animateProgress();
</script>
10.5.2 错误状态处理
<!-- 错误状态组件 -->
<div class="error-states-demo space-y-8">
<!-- 网络错误 -->
<div class="bg-red-50 border border-red-200 rounded-lg p-6">
<div class="flex items-center">
<div class="flex-shrink-0">
<svg class="h-8 w-8 text-red-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div class="ml-3 flex-1">
<h3 class="text-sm font-medium text-red-800">网络连接错误</h3>
<p class="mt-1 text-sm text-red-700">无法连接到服务器,请检查您的网络连接。</p>
<div class="mt-4">
<button class="bg-red-100 hover:bg-red-200 text-red-800 px-4 py-2 rounded-md text-sm font-medium transition-colors duration-200">
重试
</button>
</div>
</div>
</div>
</div>
<!-- 404 错误 -->
<div class="text-center py-12">
<svg class="mx-auto h-24 w-24 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1" d="M9.172 16.172a4 4 0 015.656 0M9 12h6m-6-4h6m2 5.291A7.962 7.962 0 0112 15c-2.34 0-4.47-.881-6.08-2.33" />
</svg>
<h2 class="mt-6 text-3xl font-bold text-gray-900">页面未找到</h2>
<p class="mt-2 text-lg text-gray-600">抱歉,您访问的页面不存在。</p>
<div class="mt-6">
<button class="bg-blue-500 hover:bg-blue-600 text-white px-6 py-3 rounded-lg font-medium transition-colors duration-200">
返回首页
</button>
</div>
</div>
<!-- 空状态 -->
<div class="text-center py-12">
<svg class="mx-auto h-16 w-16 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" />
</svg>
<h3 class="mt-4 text-lg font-medium text-gray-900">暂无数据</h3>
<p class="mt-2 text-gray-600">还没有任何内容,开始创建第一个项目吧!</p>
<div class="mt-6">
<button class="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded-lg font-medium transition-colors duration-200">
创建项目
</button>
</div>
</div>
</div>
10.5.3 响应式优化
/* 响应式优化 CSS */
@layer utilities {
/* 容器查询 */
.container-responsive {
container-type: inline-size;
}
@container (min-width: 400px) {
.container-responsive .card {
@apply grid-cols-2;
}
}
@container (min-width: 600px) {
.container-responsive .card {
@apply grid-cols-3;
}
}
/* 字体大小响应式 */
.text-responsive {
font-size: clamp(1rem, 2.5vw, 2rem);
}
/* 间距响应式 */
.spacing-responsive {
padding: clamp(1rem, 5vw, 3rem);
}
/* 网格响应式 */
.grid-responsive {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr));
gap: clamp(1rem, 3vw, 2rem);
}
}
<!-- 响应式组件示例 -->
<div class="container-responsive">
<div class="grid grid-cols-1 card gap-4 p-4">
<div class="bg-white rounded-lg shadow-md p-6">
<h2 class="text-responsive font-bold mb-4">响应式标题</h2>
<p class="text-gray-600">这是一个响应式卡片组件,会根据容器大小自动调整布局。</p>
</div>
<div class="bg-white rounded-lg shadow-md p-6">
<h2 class="text-responsive font-bold mb-4">自适应内容</h2>
<p class="text-gray-600">使用容器查询和 clamp() 函数实现真正的响应式设计。</p>
</div>
<div class="bg-white rounded-lg shadow-md p-6">
<h2 class="text-responsive font-bold mb-4">流体布局</h2>
<p class="text-gray-600">布局会根据可用空间自动调整,提供最佳的用户体验。</p>
</div>
</div>
</div>
10.6 性能监控与分析
10.6.1 性能指标收集
// performance-monitor.js
class PerformanceMonitor {
constructor(options = {}) {
this.apiEndpoint = options.apiEndpoint || '/api/performance';
this.sampleRate = options.sampleRate || 0.1; // 10% 采样率
this.metrics = {};
this.init();
}
init() {
// 监听页面加载性能
window.addEventListener('load', () => {
setTimeout(() => this.collectLoadMetrics(), 0);
});
// 监听 Web Vitals
this.observeWebVitals();
// 监听资源加载
this.observeResourceTiming();
// 监听用户交互
this.observeUserInteractions();
}
collectLoadMetrics() {
const navigation = performance.getEntriesByType('navigation')[0];
const paint = performance.getEntriesByType('paint');
this.metrics.loadTime = {
domContentLoaded: navigation.domContentLoadedEventEnd - navigation.domContentLoadedEventStart,
loadComplete: navigation.loadEventEnd - navigation.loadEventStart,
firstPaint: paint.find(p => p.name === 'first-paint')?.startTime || 0,
firstContentfulPaint: paint.find(p => p.name === 'first-contentful-paint')?.startTime || 0,
ttfb: navigation.responseStart - navigation.requestStart,
};
this.sendMetrics('load', this.metrics.loadTime);
}
observeWebVitals() {
// Largest Contentful Paint (LCP)
if ('PerformanceObserver' in window) {
const lcpObserver = new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
this.metrics.lcp = lastEntry.startTime;
this.sendMetrics('lcp', { value: lastEntry.startTime });
});
lcpObserver.observe({ entryTypes: ['largest-contentful-paint'] });
}
// First Input Delay (FID)
if ('PerformanceObserver' in window) {
const fidObserver = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
this.metrics.fid = entry.processingStart - entry.startTime;
this.sendMetrics('fid', { value: entry.processingStart - entry.startTime });
});
});
fidObserver.observe({ entryTypes: ['first-input'] });
}
// Cumulative Layout Shift (CLS)
if ('PerformanceObserver' in window) {
let clsValue = 0;
const clsObserver = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
if (!entry.hadRecentInput) {
clsValue += entry.value;
}
});
this.metrics.cls = clsValue;
});
clsObserver.observe({ entryTypes: ['layout-shift'] });
// 页面卸载时发送 CLS 数据
window.addEventListener('beforeunload', () => {
this.sendMetrics('cls', { value: clsValue });
});
}
}
observeResourceTiming() {
if ('PerformanceObserver' in window) {
const resourceObserver = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
if (entry.name.includes('.css')) {
this.sendMetrics('css-load', {
url: entry.name,
duration: entry.duration,
size: entry.transferSize,
});
}
});
});
resourceObserver.observe({ entryTypes: ['resource'] });
}
}
observeUserInteractions() {
// 点击响应时间
document.addEventListener('click', (event) => {
const startTime = performance.now();
requestAnimationFrame(() => {
const endTime = performance.now();
const duration = endTime - startTime;
if (duration > 100) { // 只记录较慢的交互
this.sendMetrics('interaction', {
type: 'click',
target: event.target.tagName,
duration: duration,
});
}
});
});
}
sendMetrics(type, data) {
// 采样控制
if (Math.random() > this.sampleRate) {
return;
}
const payload = {
type,
data,
timestamp: Date.now(),
url: window.location.href,
userAgent: navigator.userAgent,
connection: this.getConnectionInfo(),
};
// 使用 sendBeacon 或 fetch 发送数据
if (navigator.sendBeacon) {
navigator.sendBeacon(this.apiEndpoint, JSON.stringify(payload));
} else {
fetch(this.apiEndpoint, {
method: 'POST',
body: JSON.stringify(payload),
headers: {
'Content-Type': 'application/json',
},
keepalive: true,
}).catch(console.error);
}
}
getConnectionInfo() {
if ('connection' in navigator) {
const conn = navigator.connection;
return {
effectiveType: conn.effectiveType,
downlink: conn.downlink,
rtt: conn.rtt,
};
}
return null;
}
// 手动记录自定义指标
recordCustomMetric(name, value, tags = {}) {
this.sendMetrics('custom', {
name,
value,
tags,
});
}
}
// 初始化性能监控
const monitor = new PerformanceMonitor({
apiEndpoint: '/api/performance',
sampleRate: 0.1,
});
// 使用示例
monitor.recordCustomMetric('css-build-size', 125.5, { version: '1.0.0' });
10.6.2 性能报告生成
”`javascript // performance-report.js class PerformanceReport { constructor() { this.data = []; this.thresholds = { lcp: 2500, // 2.5s fid: 100, // 100ms cls: 0.1, // 0.1 ttfb: 600, // 600ms }; }
async generateReport() {
const report = {
timestamp: new Date().toISOString(),
summary: await this.getSummary(),
details: await this.getDetails(),
recommendations: this.getRecommendations(),
};
return report;
}
async getSummary() {
const metrics = await this.collectCurrentMetrics();
return {
score: this.calculateScore(metrics),
metrics: metrics,
status: this.getStatus(metrics),
};
}
async collectCurrentMetrics() {
return new Promise((resolve) => {
const metrics = {};
// 收集导航时间
const navigation = performance.getEntriesByType('navigation')[0];
if (navigation) {
metrics.ttfb = navigation.responseStart - navigation.requestStart;
metrics.domContentLoaded = navigation.domContentLoadedEventEnd - navigation.domContentLoadedEventStart;
metrics.loadComplete = navigation.loadEventEnd - navigation.loadEventStart;
}
// 收集绘制时间
const paint = performance.getEntriesByType('paint');
metrics.firstPaint = paint.find(p => p.name === 'first-paint')?.startTime || 0;
metrics.firstContentfulPaint = paint.find(p => p.name === 'first-contentful-paint')?.startTime || 0;
// 收集资源信息
const resources = performance.getEntriesByType('resource');
const cssResources = resources.filter(r => r.name.includes('.css'));
metrics.cssLoadTime = cssResources.reduce((total, resource) => {
return total + resource.duration;
}, 0);
metrics.cssSize = cssResources.reduce((total, resource) => {
return total + (resource.transferSize || 0);
}, 0);
resolve(metrics);
});
}
calculateScore(metrics) {
let score = 100;
// LCP 评分
if (metrics.firstContentfulPaint > this.thresholds.lcp) {
score -= 20;
} else if (metrics.firstContentfulPaint > this.thresholds.lcp * 0.75) {
score -= 10;
}
// TTFB 评分
if (metrics.ttfb > this.thresholds.ttfb) {
score -= 15;
} else if (metrics.ttfb > this.thresholds.ttfb * 0.75) {
score -= 8;
}
// CSS 大小评分
if (metrics.cssSize > 100 * 1024) { // 100KB
score -= 15;
} else if (metrics.cssSize > 50 * 1024) { // 50KB
score -= 8;
}
// CSS 加载时间评分
if (metrics.cssLoadTime > 1000) { // 1s
score -= 10;
} else if (metrics.cssLoadTime > 500) { // 500ms
score -= 5;
}
return Math.max(0, score);
}
getStatus(metrics) {
const score = this.calculateScore(metrics);
if (score >= 90) return 'excellent';
if (score >= 75) return 'good';
if (score >= 60) return 'needs-improvement';
return 'poor';
}
getRecommendations() {
const recommendations = [];
// 基于当前指标生成建议
recommendations.push({
category: 'CSS 优化',
items: [
'启用 CSS 压缩和合并',
'移除未使用的 CSS 规则',
'使用 CSS 预加载',
'考虑关键 CSS 内联',
],
});
recommendations.push({
category: '图片优化',
items: [
'使用现代图片格式 (WebP, AVIF)',
'实现图片懒加载',
'优化图片尺寸和质量',
'使用响应式图片',
],
});
recommendations.push({
category: '缓存策略',
items: [
'设置适当的缓存头',
'使用 CDN 加速',
'启用浏览器缓存',
'实现服务端缓存',
],
});
return recommendations;
}
async exportReport(format = 'json') {
const report = await this.generateReport();
switch (format) {
case 'json':
return JSON.stringify(report, null, 2);
case 'html':
return this.generateHTMLReport(report);
case 'csv':
return this.generateCSVReport(report);
default:
return report;
}
}
generateHTMLReport(report) {
return `
<!DOCTYPE html>
性能分析报告
<!-- 总体评分 -->
<div class="bg-white rounded-lg shadow-md p-6 mb-8">
<h2 class="text-xl font-semibold mb-4">总体评分</h2>
<div class="flex items-center space-x-4">
<div class="text-4xl font-bold ${this.getScoreColor(report.summary.score)}">
${report.summary.score}
</div>
<div>
<div class="text-lg font-medium ${this.getStatusColor(report.summary.status)}">
${this.getStatusText(report.summary.status)}
</div>
<div class="text-gray-600">生成时间: ${new Date(report.timestamp).toLocaleString()}</div>
</div>
</div>
</div>
<!-- 详细指标 -->
<div class="bg-white rounded-lg shadow-md p-6 mb-8">
<h2 class="text-xl font-semibold mb-4">详细指标</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
${Object.entries(report.summary.metrics).map(([key, value]) => `
<div class="bg-gray-50 p-4 rounded">
<div class="text-sm text-gray-600">${this.getMetricName(key)}</div>
<div class="text-lg font-semibold">${this.formatMetricValue(key, value)}</div>
</div>
`).join('')}
</div>
</div>
<!-- 优化建议 -->
<div class="bg-white rounded-lg shadow-md p-6">
<h2 class="text-xl font-semibold mb-4">优化建议</h2>
${report.recommendations.map(category => `
<div class="mb-6">
<h3 class="text-lg font-medium mb-2">${category.category}</h3>
<ul class="list-disc list-inside space-y-1">
${category.items.map(item => `<li class="text-gray-700">${item}</li>`).join('')}
</ul>
</div>
`).join('')}
</div>
</div>
`; }