⽹络层⾯优化:让你的⽹站跑得⽐快递员还快

1. 浏览器缓存:别让⽤户重复下载你的"祖传代码"

兄弟们,有没有遇到过这种情况?你明明已经优化过代码了,⽤户还是说"⽹站怎么还是那个样⼦?"然后你⼀脸懵逼地发现:浏览器缓存还在使⽤你上个月写的"屎⼭代码"。

浏览器缓存就像是那个记性超好的前女友,你改了什么她都记得,但就是不愿意承认你已经变了。

1.1 LocalStorage ⼀把梭?快住手!

很多同学喜欢把什么东西都往 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')

1.2 SessionStorage 是"露⽔情缘"

关掉标签页就拜拜,不多废话。适合存那些"⽤完即弃"的数据,⽐如表单暂存。

// 表单暂存示例,防止用户误操作丢失数据
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')
})

1.3 IndexedDB?那是真的猛

当你需要存点⼤东西的时候,⽐如离线缓存、图片啥的,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)
}

1.4 Cache API:PWA 的灵魂

想做 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)
          }
        })
      )
    })
  )
})

1.5 小贴士

  • 给缓存加点过期时间,别让⽤户的手机变成了你的"垃圾场"
  • 敏感数据别存缓存,不然哪天被抓包了别怪我没提醒你
  • 定期清理缓存,就像打扫房间⼀样重要

2. 资源压缩:让体积缩⽔,让速度起飞

说真的,资源压缩是最简单也最有效的优化手段。⽤好了,能减少 70%+ 的传输体积;⽤不好,⽤户加载时就像在等快递。

2.1 Gzip/Brotli 压缩

这个主要是后端和运维配置的,但前端也要知道原理。Gzip 能压缩到原体积的 30-40%,Brotli 更强,能到 20-30%。

// Vite 开发环境也开启压缩
// vite.config.js
import { defineConfig } from 'vite'

export default defineConfig({
  server: {
    headers: {
      'Content-Encoding': 'gzip'
    }
  }
})

告诉后端大佬:

  • HTML、CSS、JS、JSON 都要开启 Gzip/Brotli
  • 图片、视频这类已经压缩过的文件就不用再压了
  • 生产环境优先用 Brotli,兼容性用 Gzip

2.2 代码压缩:Tree Shaking + 压缩工具

现代构建工具(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
      }
    }
  }
})

2.3 图片压缩:体积减半,体验翻倍

很多同学直接把设计师给的原图放上去,一张图好几 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' }
        ]
      }
    })
  ]
})

更简单的方法:用在线工具

  • TinyPNG:免费,压缩效果好
  • Squoosh:Google 出品,功能强大
  • ImageOptim:Mac 专用,拖进去就行

2.4 WebP 格式:新时代的图片格式

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
    })
  ]
})

2.5 雪碧图 vs SVG Sprite

过去常用雪碧图,现在更推荐 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>

2.6 按需加载:只加载需要的

//  一次性加载所有组件
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 }
]

3. DNS 缓存:让你的⽹址解析⽐点外卖还快

DNS 解析就像是查电话本,你每次访问⽹站都要先查⼀下 IP 地址。但如果每次都要查,那就太慢了。

3.1 DNS 预取(DNS Prefetching)

提前告诉浏览器"嘿,等下⼉可能要去这个域名,先把电话查好"。

<!-- 在 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">

使用场景:

  • CDN 域名
  • 第三方 API 域名
  • 统计分析域名(如 Google Analytics)
  • 字体服务域名(如 Google Fonts)

3.2 预连接(Preconnect)

预连接比 DNS 预取更进一步,它不仅解析 DNS,还会建立 TCP 连接和 TLS 握手。

<!-- 适用于必定会使用的域名 -->
<link rel="preconnect" href="https://cdn.example.com">
<link rel="preconnect" href="https://api.example.com">

注意: 不要滥用,每个预连接都会消耗资源。只用于必定会加载的域名。

3.3 域名收敛

一个网站用太多域名会降低性能。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">

实战经验:

  • 静态资源用 1 个 CDN 域名
  • API 用 1 个域名
  • 总共不超过 2-3 个域名

4. CDN:让你的⽹站遍布全球

CDN 是什么?简单说,就是把你的⽹站复制到世界各地,让⽤户访问最近的节点。

4.1 CDN 的好处

  • 速度提升:⽤户访问就近节点,延迟降低
  • 减轻服务器压力:流量分摊到各个节点
  • 提升可用性:某个节点挂了,其他节点还能⽤

4.2 前端资源文件名 hash 策略

这是前端使用 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]'
      }
    }
  }
})

4.3 Cloudflare Workers 前端实战

现在的 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()
}

4.4 实战案例

之前一个视频⽹站,⽤户遍布全球。没⽤ CDN 的时候,国外⽤户加载要 30 秒;⽤了 CDN 之后,⼤部分地区 3 秒内加载完成。这提升,⾹!

4.5 注意事项

  • CDN 节点的缓存要及时更新,不然你改了 bug ⽤户还是看旧版
  • HTTPS 证书要配好,CDN 和源站都要配
  • ⽇志收集要考虑 CDN 的影响,不然统计数据会偏
  • 告诉运维大佬:静态资源(带 hash 的)可以设置长期缓存,index.html 不要缓存

5. 渲染层⾯优化:让页⾯流畅如丝

网络层面的优化让资源加载更快,但渲染层面的优化让用户体验更好。

5.1 关键渲染路径优化

浏览器渲染页面的流程: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>

5.2 图片懒加载:只加载用户能看到的

// 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>

5.3 虚拟列表:长列表性能救星

如果列表有几千条数据,全部渲染会让页面卡顿。虚拟列表只渲染可见区域的项目。

// 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>

5.4 防抖和节流:优化高频事件

// 防抖:只执行最后一次
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))

5.5 代码分割:按需加载

// 路由懒加载
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()
  }
}

5.6 减少 DOM 操作

//  频繁操作 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

5.7 使用 CSS3 动画代替 JS 动画

/*  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'
}

5.8 Web Worker:把繁重计算放到后台线程

// 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

完整配置(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
      }
    }
  }
})

HTML 优化(index.html)

<!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>

性能监控(utils/performance.js)

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()

总结

性能优化不是⼀蹴⽽就的,需要持续迭代。记住⼏个原则:

  1. 能缓存的尽量缓存,让⽤户少等⼏秒
  2. 能压缩的尽量压缩,体积小了传输就快
  3. DNS 预取和预连接,提前做好准备
  4. CDN 是神器,让全球用户都能快速访问
  5. 渲染优化很重要,加载快不等于体验好
  6. 懒加载和按需加载,只加载用户需要的
  7. 监控要跟上,不然你都不知道优化有没有⽤

最后,如果你觉得这篇⽂章对你有帮助,点个赞呗!如果觉得有问题,评论区喷我,我抗揍。


本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:alixiixcom@163.com