Skip to content

提高网站加载速度,避免用户焦头烂额

更新: 2025/10/15 字数: 0 字 时长: 0 分钟

一、服务器上的配置

CDN缓存

提前从原服务器获取到网站资源,转发缓存到全国各地的CDN节点,用户在进行网站访问时,就可以就近选择最近的节点获取资源,避免远距离传输的效率低下问题。

image-20250808001712807

优点:延迟更低、允许更多人访问

缺点:在服务器上设置好缓存规则,否则会被恶意刷取流量(按流量计费,贵)

浏览器缓存

更直接的方法,在用户第一次访问后缓存到本地,下一次访问时直接本地加载

缓存的内容可以根据类型不同,设置不一样的时间,例如静态资源,可以极长时间缓存(1年以上);动态资源,html页面(可以设置几分钟到几小时);敏感资源等,不进行缓存,避免泄露。

升级HTTP2

HTTP1处理逻辑是串行处理,虽然可以建立多个连接,但是连接内的请求要按顺序处理,容易产生队头阻塞问题,而HTTP2可以多路复用,真正意义上在单个连接上处理多个请求,实现并行处理

1.可在宝塔网站中的配置文件处,添加一行代码即可

2.如果使用CDN,可以在CDN中自行勾选打卡这个配置

image-20250808002156221

宝塔中的配置

image-20250808002411464

#关键:在443端口后添加 http2
listen 443 ssl http2;

当然,HTTP3也可以使用,理论上建立连接速度更快,完全解决队头阻塞问题,不过兼容性和稳定性有待验证,暂时不考虑

二、网站本体修改

静态资源压缩

对于网站的静态资源如图片视频,大家也都清楚,体积越大,传输速度越慢,所以我们可以考虑将这些内容进行压缩。

jpg、png等图片可以进行webp格式压缩,能减少20%-90%左右的体积。

网站中如果有用户上传图片的功能,要在后端进行压缩图片代码的编写,压缩完再上传服务器,否则原图较大会消耗大量流量

python批处理的代码如下,使用示例:

#80是压缩质量 -c是开始前清除输出目录
python compress_images.py -i my_photos -o optimized_webp -q 85 -c
代码
批量压缩图片
python
import os
import time
from datetime import timedelta
from PIL import Image
import argparse
import shutil

def compress_images(input_folder, output_folder, quality=75):
    """
    将输入文件夹中的图片压缩为WebP格式并保存到输出文件夹
    
    参数:
    input_folder: 输入图片文件夹路径
    output_folder: 输出文件夹路径
    quality: WebP压缩质量 (0-100)
    """
    # 支持的图片格式
    valid_extensions = ('.jpg', '.jpeg', '.png', '.bmp', '.tiff', '.gif')
    
    # 确保输出文件夹存在
    os.makedirs(output_folder, exist_ok=True)
    
    # 初始化统计变量
    total_files = 0
    processed_files = 0
    skipped_files = 0
    original_total_size = 0
    compressed_total_size = 0
    start_time = time.time()
    
    print(f"📂 源文件夹: {os.path.abspath(input_folder)}")
    print(f"💾 目标文件夹: {os.path.abspath(output_folder)}")
    print(f"⚙️ 压缩质量: {quality}\n")
    print("=" * 60)
    
    # 遍历输入文件夹
    for filename in os.listdir(input_folder):
        input_path = os.path.join(input_folder, filename)
        
        # 检查文件扩展名
        if filename.lower().endswith(valid_extensions):
            total_files += 1
            
            # 获取原始文件大小
            original_size = os.path.getsize(input_path)
            original_total_size += original_size
            
            # 创建输出路径
            output_filename = os.path.splitext(filename)[0] + '.webp'
            output_path = os.path.join(output_folder, output_filename)
            
            try:
                # 打开并转换图片
                with Image.open(input_path) as img:
                    if img.mode in ('RGBA', 'LA', 'P'):
                        img = img.convert('RGBA')
                    else:
                        img = img.convert('RGB')
                    
                    # 保存为WebP格式
                    img.save(output_path, 'WEBP', quality=quality, method=6)
                
                # 获取压缩后文件大小
                compressed_size = os.path.getsize(output_path)
                compressed_total_size += compressed_size
                processed_files += 1
                
                # 计算单个文件压缩率
                ratio = (original_size - compressed_size) / original_size * 100
                
                # 打印单个文件结果
                print(f"[{processed_files}/{total_files}] ✓ {filename}{output_filename}")
                print(f"   原始: {format_size(original_size)} → 压缩后: {format_size(compressed_size)}")
                print(f"   节省: {format_size(original_size - compressed_size)} ({ratio:.1f}%)\n")
                
            except Exception as e:
                skipped_files += 1
                print(f"✗ 跳过 {filename} - 错误: {str(e)}\n")
    
    # 计算总处理时间
    processing_time = time.time() - start_time
    
    # 打印最终统计信息
    print("=" * 60)
    print("📊 转换统计:")
    print(f"  扫描文件总数: {total_files}")
    print(f"  成功转换: {processed_files}")
    print(f"  跳过文件: {skipped_files}")
    print(f"  处理时间: {timedelta(seconds=round(processing_time))}")
    
    # 只有处理了文件才显示压缩统计
    if processed_files > 0:
        # 计算总体压缩率
        overall_ratio = (original_total_size - compressed_total_size) / original_total_size * 100
        
        print("\n📦 体积统计:")
        print(f"  原始总大小: {format_size(original_total_size)}")
        print(f"  压缩后总大小: {format_size(compressed_total_size)}")
        print(f"  总节省空间: {format_size(original_total_size - compressed_total_size)}")
        print(f"  总体压缩率: {overall_ratio:.1f}%")
        
        # 计算平均压缩率
        avg_original = original_total_size / processed_files
        avg_compressed = compressed_total_size / processed_files
        avg_ratio = (avg_original - avg_compressed) / avg_original * 100
        print(f"\n📈 平均每张图片:")
        print(f"  原始: {format_size(avg_original)} → 压缩后: {format_size(avg_compressed)}")
        print(f"  平均节省: {format_size(avg_original - avg_compressed)} ({avg_ratio:.1f}%)")
    
    # 输出文件夹信息
    print(f"\n✅ 处理完成! 输出目录: {os.path.abspath(output_folder)}")

def format_size(size_bytes):
    """将字节大小转换为易读的格式"""
    if size_bytes < 1024:
        return f"{size_bytes} bytes"
    elif size_bytes < 1024 * 1024:
        return f"{size_bytes / 1024:.2f} KB"
    elif size_bytes < 1024 * 1024 * 1024:
        return f"{size_bytes / (1024 * 1024):.2f} MB"
    else:
        return f"{size_bytes / (1024 * 1024 * 1024):.2f} GB"

if __name__ == "__main__":
    # 设置命令行参数
    parser = argparse.ArgumentParser(
        description='📸 批量图片压缩工具 (转换为WebP格式)',
        formatter_class=argparse.ArgumentDefaultsHelpFormatter
    )
    parser.add_argument('-i', '--input', default='images',
                        help='输入文件夹路径 (默认: images)')
    parser.add_argument('-o', '--output', default='compressed_webp',
                        help='输出文件夹路径 (默认: compressed_webp)')
    parser.add_argument('-q', '--quality', type=int, default=75,
                        help='压缩质量 (0-100, 默认: 75)')
    parser.add_argument('-c', '--clean', action='store_true',
                        help='清空输出目录(如果存在)')
    
    args = parser.parse_args()
    
    # 验证质量参数
    if args.quality < 0 or args.quality > 100:
        print("错误: 质量参数必须在0-100之间")
        exit(1)
    
    # 如果需要清空输出目录
    if args.clean and os.path.exists(args.output):
        shutil.rmtree(args.output)
        print(f"已清空输出目录: {args.output}")
    
    # 执行转换
    compress_images(
        input_folder=args.input,
        output_folder=args.output,
        quality=args.quality
    )

手动代码压缩

对于CSS、JS代码、注释代码等,可以进行压缩处理,方式:注释代码可以删除,变量名缩短等等,一般的前端打包工具可以进行压缩

function getCompressConfig() {
  return {
    minify: 'terser',//使用 Terser 进行JS压缩cssMinify:true,/启用CSS压编terserOptions:{
    cssMinify:true,
    terserOptions:{
      compress:{
        drop_console:true,//移除所有console.log语句
        drop_debugger:true,//移除所有debugger语句
        pure_funcs:['console.log'],// 额外指定要移除的纯函数
        passes:2// 执行 2轮压缩优化
      },
      mangle:{
        toplevel:true//混淆顶级作用域的变量名
      },
      format:{
        comments:false// 移除所有代码注释
      }
    }
  }
}

自动gzip压缩

浏览器和服务器间有协议,浏览器在请求头告诉服务器开启了gzip压缩,服务器收到请求后即可开启压缩,将文件压缩后再上传,通过请求头告诉浏览器是压缩后的文件,然后浏览器自动解压

image-20250808005846658

三、加载策略

懒加载

延迟加载用户看不到的内容,当用户查看到时才加载,这个可以显著提升首屏显示时间(适合滚动 的元素)

模块化按需加载

网站的css、js拆分成多个文件,访问到某个页面时,只加载对于页面的css和js,也能提升访问速度

分层加载

先让用户看到内容,点击访问时才加载高质量内容,例如预览显示缩略图,点击后才显示高清图

渐进式加载

和分层类似但更高级,比如图片先显示模糊的图片预览,然后逐渐清晰,这样子能保证元素不会错位等

预加载

通常用于文章等内容,当用户访问文章列表时,预加载某些文章的内容,点击详情时用户即可实现无感秒进的效果(但是要注意加载的数量,根据情况加载)

四、请求合并

浏览器可能对同一域名有请求限制,所以要调用多次后端接口请求数据时,可能产生排队和阻塞,可以修改后端代码,合并请求,也可以搭建node中间层来做

tips:图标文件可以整合成一个长宽规范的文件,利用css雪碧图特性,把所有图标合并成一张,在前端利用css的background position加载图片指定位置


2025/10/15

继述

在进行HyperUI网站的开发时,第一次使用了chrome预渲染的新特性:Speculation Rules API

预计用户会访问这些URL,可以提前预加载或预渲染它们

有两种形式,分为Prefetch(预取)Prerender(预渲染),前者是只下载页面资源,包括HTML、图片、脚本等,但是不执行,后者真正加载并渲染整个界面,但在后台不可见,当用户点击后实现**“瞬间切换”**!

欸,有没有这么神奇我也不知道 ( ̄、 ̄)

1.声明speculation Rules JSON

代码示例如下,在index.html添加script,告诉浏览器哪些页面要预渲染

html
<script type="speculationrules">
{
  "prerender": [
    { "source": "list", "urls": ["/next-page.html"] }
  ]
}
</script>

字段详解

字段含义
source来源类型,可为 "list""document"
"list"直接列出要预渲染的 URL
"document"从文档中选取匹配的元素(如 <a>
urls要预渲染或预取的 URL 列表
selectors用于选择 DOM 元素的 CSS 选择器

实际应用中如下,通过source定位资源类型,然后根据eagerness的期望程度,“希望浏览器在后台预渲染/xxx.html的界面”

::: example

 <script type="speculationrules">
        {
            "prerender": [
                {
                    "source": "document",
                    "where": {
                        "href_matches": "/components/*"
                    },
                    "eagerness": "moderate"
                },
                {
                    "source": "document", 
                    "where": {
                        "href_matches": "/create"
                    },
                    "eagerness": "moderate"
                },
                {
                    "source": "document",
                    "where": {
                        "href_matches": "/profile/*"
                    },
                    "eagerness": "moderate"
                },
                {
                    "source": "document",
                    "where": {
                        "selector_matches": "a[href^='/components/']"
                    },
                    "eagerness": "eager"
                },
                {
                    "source": "document",
                    "where": {
                        "selector_matches": ".card a, .side-link"
                    },
                    "eagerness": "moderate"
                }
            ],
            "prefetch": [
                {
                    "source": "document",
                    "where": {
                        "href_matches": "/api/*"
                    },
                    "eagerness": "moderate"
                }
            ]
        }
    </script>

::: 这种就是最基础的用法,直接写好每条页面地址的路径去获取资源预渲染,如果是使用了某些前端框架,可以参考下面的规则

好的本期文章到此结束,不用往下看了 ( ̄_, ̄ )

2.动态生成规则(常用于单页面应用SPA)

适用:Vue/React/Next.js/VitePress等前端框架

js
const speculationScript = document.createElement('script');
speculationScript.type = 'speculationrules';
speculationScript.textContent = JSON.stringify({
  prerender: [
    { source: 'list', urls: ['/profile', '/settings'] }
  ]
});
document.head.appendChild(speculationScript);

3.特定元素自动触发

这种方法就时候想在某个小元素内进行触发,而且是用户使用量最高的链接,例如个人中心,首页等,我们就可以在<a>标签中加上rel="prerender"rel="prefetch"

html
<a href="/next-page.html" rel="prerender">下一页</a>

或者动态触发,这个有点高级

js
document.querySelectorAll('a').forEach(link => {
  link.addEventListener('mouseenter', () => {
    const script = document.createElement('script');
    script.type = 'speculationrules';
    script.textContent = JSON.stringify({
      prerender: [{ source: 'list', urls: [link.href] }]
    });
    document.head.appendChild(script);
  });
});
  • 鼠标悬停后,浏览器开始预渲染目标页面;
  • 用户点击时,立即显示该页面;
  • 若用户未点击,则浏览器稍后会自动销毁预渲染页面,避免浪费资源。

调试方法

在Chrome DevTools地址栏中输入:chrome://speculation-rules

即可查看当前页面所有预渲染状态

使用策略

💡 七、实际使用策略(推荐)

场景策略
用户鼠标悬停菜单项动态注入 prerender
首页已知下一步访问路径固定 list 规则 prerender
内容加载量大但交互少使用 prerender
数据接口、图片等资源使用 prefetch

TIP

这个特性有同源特性,即智能渲染同源(域名、端口、协议相同)的页面

不支持登录状态切换,若预渲染页面依赖登录态变化,可能出错

如果浏览器资源紧张,会触发自动回收,终止prerender

本站访客数 人次 本站总访问量