二次元绘画创作
56.21M · 2026-02-04
兄弟们,有没有遇到过这种情况?你明明已经优化过代码了,⽤户还是说"⽹站怎么还是那个样⼦?"然后你⼀脸懵逼地发现:浏览器缓存还在使⽤你上个月写的"屎⼭代码"。
浏览器缓存就像是那个记性超好的前女友,你改了什么她都记得,但就是不愿意承认你已经变了。
很多同学喜欢把什么东西都往 LocalStorage ⾥塞,恨不得把整个项⽬都存进去。听我⼀句劝,LocalStorage 就像你的钱包,别什么都往⾥塞,否则哪天爆了就尴尬了。
// 错误示范
localStorage.setItem('整个项目', JSON.stringify(yourProject))
// 正确姿势:封装一下,加上过期时间
class Storage {
static set(key, value, expire = 7 * 24 * 60 * 60 * 1000) {
const data = {
value,
expire: Date.now() + expire
}
localStorage.setItem(key, JSON.stringify(data))
}
static get(key) {
const data = JSON.parse(localStorage.getItem(key))
if (!data) return null
if (Date.now() > data.expire) {
localStorage.removeItem(key)
return null
}
return data.value
}
}
// 使用起来更优雅
Storage.set('userInfo', { name: '张三', age: 18 })
const user = Storage.get('userInfo')
关掉标签页就拜拜,不多废话。适合存那些"⽤完即弃"的数据,⽐如表单暂存。
// 表单暂存示例,防止用户误操作丢失数据
const form = document.querySelector('#myForm')
// 输入时自动保存
form.addEventListener('input', () => {
const formData = new FormData(form)
sessionStorage.setItem('formData', JSON.stringify(Object.fromEntries(formData)))
})
// 页面加载时恢复
window.addEventListener('load', () => {
const saved = sessionStorage.getItem('formData')
if (saved) {
const data = JSON.parse(saved)
Object.keys(data).forEach(key => {
form.elements[key].value = data[key]
})
}
})
// 提交后清除
form.addEventListener('submit', () => {
sessionStorage.removeItem('formData')
})
当你需要存点⼤东西的时候,⽐如离线缓存、图片啥的,IndexedDB 才是你的菜。虽然 API 设计得像迷宫,但好歹是个迷宫⾥有宝藏的那种。
// ⼿写 IndexedDB 简直折磨,推荐⽤ idb 这个库
import { openDB } from 'idb'
// 初始化数据库
const db = await openDB('myDB', 1, {
upgrade(db) {
if (!db.objectStoreNames.contains('images')) {
db.createObjectStore('images', { keyPath: 'id' })
}
}
})
// 存储图片
async function saveImage(id, blob) {
await db.put('images', { id, blob, timestamp: Date.now() })
}
// 读取图片
async function getImage(id) {
return await db.get('images', id)
}
想做 PWA?Cache API 必须得会。它能让你在离线状态下也能访问⽹站,简直是⽹络开⼩差时的救命稻草。
// sw.js
const CACHE_NAME = 'my-cache-v1'
const urlsToCache = [
'/',
'/styles/main.css',
'/scripts/main.js',
'/images/logo.png'
]
// 安装时缓存静态资源
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then((cache) => cache.addAll(urlsToCache))
.then(() => self.skipWaiting())
)
})
// 拦截请求,优先从缓存读取
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then((response) => {
// 缓存命中直接返回,否则⾛网络
return response || fetch(event.request).then(response => {
// 把新资源缓存起来
return caches.open(CACHE_NAME).then(cache => {
cache.put(event.request, response.clone())
return response
})
})
})
)
})
// 更新缓存
self.addEventListener('activate', (event) => {
const cacheWhitelist = [CACHE_NAME]
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
if (cacheWhitelist.indexOf(cacheName) === -1) {
return caches.delete(cacheName)
}
})
)
})
)
})
说真的,资源压缩是最简单也最有效的优化手段。⽤好了,能减少 70%+ 的传输体积;⽤不好,⽤户加载时就像在等快递。
这个主要是后端和运维配置的,但前端也要知道原理。Gzip 能压缩到原体积的 30-40%,Brotli 更强,能到 20-30%。
// Vite 开发环境也开启压缩
// vite.config.js
import { defineConfig } from 'vite'
export default defineConfig({
server: {
headers: {
'Content-Encoding': 'gzip'
}
}
})
告诉后端大佬:
现代构建工具(Vite、Webpack)都内置了这些功能,配置好就行。
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
build: {
// 开启 CSS 代码分割
cssCodeSplit: true,
// 设置 chunk 大小警告阈值(kb)
chunkSizeWarningLimit: 1000,
// 压缩配置
minify: 'terser',
terserOptions: {
compress: {
drop_console: true, // 生产环境去掉 console
drop_debugger: true // 生产环境去掉 debugger
}
},
rollupOptions: {
output: {
// 开启 Tree Shaking
treeshake: true
}
}
}
})
很多同学直接把设计师给的原图放上去,一张图好几 MB,⽤户加载要等半天。
// vite-plugin-imagemin:自动压缩图片
import { defineConfig } from 'vite'
import viteImagemin from 'vite-plugin-imagemin'
export default defineConfig({
plugins: [
viteImagemin({
gifsicle: { optimizationLevel: 7 },
optipng: { optimizationLevel: 7 },
mozjpeg: { quality: 80 },
pngquant: { quality: [0.8, 0.9] },
svgo: {
plugins: [
{ name: 'removeViewBox' },
{ name: 'removeEmptyAttrs' }
]
}
})
]
})
更简单的方法:用在线工具
WebP 比传统格式(JPG、PNG)小 25-35%,质量还更好。
<!-- 传统写法 -->
<img src="image.jpg" alt="示例图片">
<!-- 优化写法:支持 WebP 用 WebP,不支持降级到 JPG -->
<picture>
<source srcset="image.webp" type="image/webp">
<img src="image.jpg" alt="示例图片">
</picture>
// 自动转 WebP:vite-plugin-webp
import { defineConfig } from 'vite'
import webp from 'vite-plugin-webp'
export default defineConfig({
plugins: [
webp({
quality: 80,
enablePlugin: true
})
]
})
过去常用雪碧图,现在更推荐 SVG Sprite。
// vite-plugin-svg-icons
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
import path from 'path'
export default defineConfig({
plugins: [
createSvgIconsPlugin({
iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')],
symbolId: 'icon-[name]'
})
]
})
<!-- 在组件中使用 -->
<svg>
<use xlink:href="#icon-user" />
</svg>
// 一次性加载所有组件
import { Button, Input, Select, Table, Form, ... } from 'element-ui'
// 按需加载
import { Button } from 'element-ui'
// 或者用动态导入
const Table = () => import('element-ui/lib/table')
// Vue 路由懒加载
const Home = () => import('@/views/Home.vue')
const About = () => import('@/views/About.vue')
const routes = [
{ path: '/', component: Home },
{ path: '/about', component: About }
]
DNS 解析就像是查电话本,你每次访问⽹站都要先查⼀下 IP 地址。但如果每次都要查,那就太慢了。
提前告诉浏览器"嘿,等下⼉可能要去这个域名,先把电话查好"。
<!-- 在 HTML head 中添加 -->
<link rel="dns-prefetch" href="https://cdn.example.com">
<link rel="dns-prefetch" href="https://api.example.com">
<link rel="dns-prefetch" href="https://static.example.com">
使用场景:
预连接比 DNS 预取更进一步,它不仅解析 DNS,还会建立 TCP 连接和 TLS 握手。
<!-- 适用于必定会使用的域名 -->
<link rel="preconnect" href="https://cdn.example.com">
<link rel="preconnect" href="https://api.example.com">
注意: 不要滥用,每个预连接都会消耗资源。只用于必定会加载的域名。
一个网站用太多域名会降低性能。HTTP/1.1 时代需要域名分片来突破浏览器并发限制,现在 HTTP/2 多路复用反而收敛更好。
// 太多域名
<img src="https://cdn1.example.com/a.jpg">
<img src="https://cdn2.example.com/b.jpg">
<img src="https://cdn3.example.com/c.jpg">
// 收敛到 1-2 个域名
<img src="https://cdn.example.com/a.jpg">
<img src="https://cdn.example.com/b.jpg">
<img src="https://cdn.example.com/c.jpg">
实战经验:
CDN 是什么?简单说,就是把你的⽹站复制到世界各地,让⽤户访问最近的节点。
这是前端使用 CDN 的关键!只有文件名带了 hash,才能放心设置长期缓存。
// webpack.config.js
module.exports = {
output: {
filename: '[name].[contenthash:8].js',
chunkFilename: '[name].[contenthash:8].chunk.js'
},
module: {
rules: [
{
test: /.(png|jpg|gif)$/,
type: 'asset/resource',
generator: {
filename: 'images/[name].[contenthash:8][ext]'
}
}
]
}
}
// vite.config.js
import { defineConfig } from 'vite'
export default defineConfig({
build: {
rollupOptions: {
output: {
entryFileNames: 'assets/[name].[hash].js',
chunkFileNames: 'assets/[name].[hash].js',
assetFileNames: 'assets/[name].[hash].[ext]'
}
}
}
})
现在的 CDN 不止是存静态资源,还能在边缘节点运⾏代码。Cloudflare Workers 咱们前端可以自己写。
// Cloudflare Workers 示例:边缘图片压缩
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request))
})
async function handleRequest(request) {
const url = new URL(request.url)
// 检测是否是图片请求
if (url.pathname.match(/.(jpg|jpeg|png|webp)$/)) {
// 检查 URL 参数
const quality = parseInt(url.searchParams.get('quality')) || 80
const format = url.searchParams.get('format') || 'webp'
// 从源站获取图片
const response = await fetch(request)
// 转换图片格式和压缩(使用 WebAssembly 实现的图片处理库)
const image = await response.arrayBuffer()
const compressed = await compressImage(image, { quality, format })
return new Response(compressed, {
headers: {
'Content-Type': `image/${format}`,
'Cache-Control': 'public, max-age=31536000'
}
})
}
return fetch(request)
}
async function compressImage(imageBuffer, options) {
// 使用前端熟悉的库(如 sharp-wasm)
const sharp = require('sharp-wasm')
return await sharp(imageBuffer)
.webp({ quality: options.quality })
.toBuffer()
}
之前一个视频⽹站,⽤户遍布全球。没⽤ CDN 的时候,国外⽤户加载要 30 秒;⽤了 CDN 之后,⼤部分地区 3 秒内加载完成。这提升,⾹!
网络层面的优化让资源加载更快,但渲染层面的优化让用户体验更好。
浏览器渲染页面的流程:DOM → CSSOM → Render Tree → Layout → Paint
<!-- 阻塞渲染的写法 -->
<link rel="stylesheet" href="large.css">
<script src="app.js"></script> <!-- 阻塞后续内容 -->
<!-- 优化写法 -->
<!-- 内联关键 CSS -->
<style>
/* 首屏必需的样式 */
.header { height: 60px; background: #333; }
</style>
<!-- 非关键样式异步加载 -->
<link rel="preload" href="non-critical.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="non-critical.css"></noscript>
<!-- JS 放到底部或异步加载 -->
<script src="app.js" defer></script>
<script src="analytics.js" async></script>
// Intersection Observer API(现代浏览器支持)
const lazyImages = document.querySelectorAll('img.lazy')
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target
img.src = img.dataset.src
img.classList.remove('lazy')
observer.unobserve(img)
}
})
})
lazyImages.forEach(img => imageObserver.observe(img))
<!-- HTML 使用方式 -->
<img class="lazy" data-src="image.jpg" alt="示例图片">
<!-- Vue 懒加载组件 -->
<template>
<img v-lazy="imageUrl" alt="示例图片">
</template>
<script>
import { lazyLoad } from '@/directives/lazyLoad'
export default {
directives: {
lazy: lazyLoad
},
data() {
return {
imageUrl: 'https://example.com/image.jpg'
}
}
}
</script>
如果列表有几千条数据,全部渲染会让页面卡顿。虚拟列表只渲染可见区域的项目。
// vue-virtual-scroller 示例
import { RecycleScroller } from 'vue-virtual-scroller'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
export default {
components: {
RecycleScroller
},
data() {
return {
items: Array.from({ length: 10000 }, (_, i) => ({
id: i,
text: `Item ${i}`
}))
}
}
}
<template>
<RecycleScroller
class="scroller"
:items="items"
:item-size="50"
key-field="id"
>
<template #default="{ item }">
<div class="item">{{ item.text }}</div>
</template>
</RecycleScroller>
</template>
<style>
.scroller {
height: 400px;
}
.item {
height: 50px;
line-height: 50px;
border-bottom: 1px solid #eee;
}
</style>
// 防抖:只执行最后一次
function debounce(fn, delay) {
let timer
return function(...args) {
clearTimeout(timer)
timer = setTimeout(() => fn.apply(this, args), delay)
}
}
// 节流:固定时间执行一次
function throttle(fn, delay) {
let lastTime = 0
return function(...args) {
const now = Date.now()
if (now - lastTime >= delay) {
lastTime = now
fn.apply(this, args)
}
}
}
// 使用示例
const searchInput = document.getElementById('search')
// 搜索输入防抖
searchInput.addEventListener('input', debounce((e) => {
console.log('搜索:', e.target.value)
}, 300))
// 滚动节流
window.addEventListener('scroll', throttle(() => {
console.log('滚动位置:', window.scrollY)
}, 100))
// 路由懒加载
const routes = [
{
path: '/home',
component: () => import('@/views/Home.vue')
},
{
path: '/about',
component: () => import('@/views/About.vue')
}
]
// 组件懒加载
export default {
components: {
HeavyComponent: () => import('@/components/HeavyComponent.vue')
}
}
// 条件加载
async function loadModule() {
if (needsFeature) {
const module = await import('@/features/advanced')
module.doSomething()
}
}
// 频繁操作 DOM
for (let i = 0; i < 1000; i++) {
const div = document.createElement('div')
div.textContent = `Item ${i}`
document.body.appendChild(div)
}
// 使用 DocumentFragment
const fragment = document.createDocumentFragment()
for (let i = 0; i < 1000; i++) {
const div = document.createElement('div')
div.textContent = `Item ${i}`
fragment.appendChild(div)
}
document.body.appendChild(fragment)
// 或者使用 innerHTML 一次性插入
let html = ''
for (let i = 0; i < 1000; i++) {
html += `<div>Item ${i}</div>`
}
document.body.innerHTML = html
/* JS 动画(性能差) */
.element {
transition: none;
}
/* CSS3 动画(性能好) */
.element {
transform: translateX(100px);
transition: transform 0.3s ease;
}
/* 更好的:使用 will-change 提示浏览器优化 */
.element {
will-change: transform;
}
// JS 实现动画
function animate(element) {
let position = 0
setInterval(() => {
position += 10
element.style.left = position + 'px'
}, 16)
}
// 使用 CSS3
function animate(element) {
element.style.transform = 'translateX(100px)'
element.style.transition = 'transform 0.3s ease'
}
// main.js
const worker = new Worker('./worker.js')
worker.postMessage({ data: largeData })
worker.onmessage = (e) => {
console.log('计算结果:', e.data.result)
}
// worker.js
self.onmessage = (e) => {
const result = heavyCalculation(e.data.data)
self.postMessage({ result })
}
function heavyCalculation(data) {
// 耗时计算
return data.reduce((a, b) => a + b, 0)
}
讲了这么多,怎么组合起来用?来个真实案例,纯前端操作:
my-project/
├── public/
│ ├── index.html
│ └── favicon.ico
├── src/
│ ├── assets/
│ │ ├── images/
│ │ └── styles/
│ ├── components/
│ ├── utils/
│ │ ├── cache.js
│ │ └── request.js
│ ├── sw.js
│ └── main.js
└── vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import viteImagemin from 'vite-plugin-imagemin'
import webp from 'vite-plugin-webp'
export default defineConfig({
plugins: [
vue(),
viteImagemin({
gifsicle: { optimizationLevel: 7 },
optipng: { optimizationLevel: 7 },
mozjpeg: { quality: 80 },
svgo: {
plugins: [
{ name: 'removeViewBox' },
{ name: 'removeEmptyAttrs' }
]
}
}),
webp({
quality: 80
})
],
build: {
rollupOptions: {
output: {
entryFileNames: 'assets/[name].[hash].js',
chunkFileNames: 'assets/[name].[hash].js',
assetFileNames: 'assets/[name].[hash].[ext]'
}
},
cssCodeSplit: true,
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true
}
}
}
})
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- DNS 预取 -->
<link rel="dns-prefetch" href="https://cdn.example.com">
<link rel="preconnect" href="https://cdn.example.com">
<!-- 内联关键 CSS -->
<style>
/* 首屏必需的样式 */
body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, sans-serif; }
.header { height: 60px; background: #333; color: #fff; }
.loading { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); }
</style>
<!-- 非关键样式异步加载 -->
<link rel="preload" href="/assets/style.[hash].css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/assets/style.[hash].css"></noscript>
</head>
<body>
<div id="app">
<div class="loading">加载中...</div>
</div>
<!-- 脚本异步加载 -->
<script src="/assets/main.[hash].js" defer></script>
</body>
</html>
export function trackPerformance() {
if ('PerformanceObserver' in window) {
// 监控资源加载
const resourceObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.transferSize === 0) {
console.log(` 缓存命中: ${entry.name}`)
} else {
console.log(` 网络加载: ${entry.name} (${(entry.transferSize / 1024).toFixed(2)}KB)`)
}
}
})
resourceObserver.observe({ entryTypes: ['resource'] })
// 监控长任务
const longTaskObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.warn(`️ 长任务: ${entry.name} (${entry.duration}ms)`)
}
})
longTaskObserver.observe({ entryTypes: ['longtask'] })
}
}
// 在 main.js 中调用
trackPerformance()
性能优化不是⼀蹴⽽就的,需要持续迭代。记住⼏个原则:
最后,如果你觉得这篇⽂章对你有帮助,点个赞呗!如果觉得有问题,评论区喷我,我抗揍。