Ch3nyang's blog collections_bookmark

post

person

about

category

category

local_offer

tag

rss_feed

rss

静态网页加载速度优化实践

calendar_month 2024-11
archive 前端
tag html tag js tag webp tag dns

在 Cloudflare 不抽风的情况下,打开我博客的速度应该是快到起飞的。Google Lighthouse 测试结果如下:

Google lighthouse result

你或许认为,这是静态网页,加载速度与服务器基本没关系,因此速度快是理所当然的事情。但实际上,脚本、样式表、图片、字体等资源的加载速度,都会极大地影响用户体验。本文将介绍一些我在优化网页加载速度时用到的方法。

CDN 缓存

不论是什么样的资源,如果每次访问都从服务器加载,那么网页加载速度一定会很慢。我们可以使用 CDN 缓存来减少网络请求。

例如,我使用 Cloudflare 缓存了网站的静态资源。当用户访问网站时,Cloudflare 会将这些资源缓存到全球各地的 CDN 节点。当用户再次访问网站时,CDN 节点会直接返回缓存的资源,而不是从服务器加载资源。这样可以减少网络请求,提升网页加载速度。由于 CDN 节点通常比服务器更靠近用户,所以 CDN 缓存可以显著提升网页加载速度。

我们可以将很少变动的资源设置较长的缓存时间,例如图片、样式表、脚本;将经常变动的资源设置较短的缓存时间,例如 HTML 文件。

图片优化

图片是网页中占用空间最大的资源之一。优化图片的加载速度,可以显著提升网页的加载速度。在图片不能再降低清晰度的情况下,一个最直接的想法便是将图片全部转为 WebP 格式以减小图片体积。WebP 是一种支持有损压缩和无损压缩的图片格式,由 Google 开发。WebP 格式的图片体积通常比 JPEG 和 PNG 格式的图片体积小 25%~34%。我们可以使用 cwebp 工具将图片转为 WebP 格式。我写了一个 PowerShell 脚本,可以将指定文件夹下的所有图片转为 WebP 格式,并删除原始图片:

$inputFolder = ".\images\" # 输入文件夹
$outputFolder = ".\images\" # 输出文件夹

if (-Not (Test-Path -Path $outputFolder)) {
    New-Item -ItemType Directory -Path $outputFolder
}

$imageFiles = Get-ChildItem -Path $inputFolder -File -Include @("*.png", "*.gif", "*.jpeg", "*.jpg", "*.svg", "*.bmp", "*.tiff", "*.ico", "*.cur", "*.apng", "*.avif", "*.heif", "*.heic") -Recurse

if ($imageFiles.Count -eq 0) {
    Write-Output "No image files found in the input folder."
} else {
    foreach ($imageFile in $imageFiles) {
        $outputFile = Join-Path -Path $outputFolder -ChildPath ($imageFile.BaseName + ".webp")
        & cwebp $imageFile.FullName -o $outputFile
    }
    Write-Output "Conversion completed!"
}

$deleteOriginal = Read-Host "Do you want to delete the original images? (Y/N)"
if ($deleteOriginal -eq "Y" -or $deleteOriginal -eq "y") {
    foreach ($imageFile in $imageFiles) {
        Remove-Item -Path $imageFile.FullName
    }
    Write-Output "Original images deleted!"
} else {
    Write-Output "Original images not deleted."
}

仅仅压缩图片体积是不够的。在实际加载时,没有浏览到的图片不应该加载。我们可以使用 loading="lazy" 属性延迟加载图片。这个属性告诉浏览器,只有当图片进入视口时才加载图片:

<img src="image.webp" alt="Image" loading="lazy">

对于网页中一些使用图片做的图标,我们可以将多个图标合并到一张图片中,然后使用 CSS background-position 属性显示图标。这样可以减少图片数量,从而减少请求次数:

.icon {
    width: 16px;
    height: 16px;
    background-image: url('icons.png');
}

.icon1 {
    background-position: 0 0;
}

.icon2 {
    background-position: -16px 0;
}

样式表优化

网页优化中有个很重要的指标叫 First Contentful Paint(FCP),即首次内容绘制。FCP 是浏览器渲染第一个 DOM 元素的时间。CSS 文件的加载速度会影响 FCP。在 CSS 文件较小的情况下,我们可以将 CSS 文件直接嵌入到 HTML 文件中,这样可以减少一次网络请求。如果 CSS 文件较大,我们可以将其拆分:先加载一个最小的 CSS 文件以正确渲染出内容;然后使用 JavaScript 动态加载其他 CSS 文件,丰富页面样式。

CSS 文件本身也很重要。通常,我推荐使用 class 选择器,而不是 tag 选择器。tag 选择器可能会命中很多元素,导致浏览器渲染速度变慢。而 class 选择器只会命中指定的元素,可以提升渲染速度。此外,我们可以使用 will-change 属性告诉浏览器哪些元素会发生变化,这样浏览器可以提前做好准备,提升渲染速度:

.element {
    will-change: transform;
}

字体优化

字体文件也是网页中常见的资源。字体文件较大,加载速度较慢。我们可以使用 font-display 属性优化字体加载。font-display 属性控制字体加载的行为。常见的值有:

  • auto:默认值。浏览器根据网络状况决定是否使用本地字体。
  • block:浏览器等待字体加载完成后再渲染文本。
  • swap:浏览器渲染文本时使用系统字体,等字体加载完成后再替换为自定义字体。
  • fallback:类似 swap,但是在字体加载失败时使用系统字体。
@font-face {
    font-family: 'CustomFont';
    src: url('custom-font.woff2') format('woff2');
    font-display: swap;
}

* {
    font-family: 'CustomFont', sans-serif;
}

同时,很多字体非常大,我们只会用到其中一小部分。unicode-range 属性可以指定字体的字符范围。这样可以减小字体体积,提升字体加载速度。例如,我们只使用了拉丁字符,可以这样设置:

@font-face {
    font-family: 'CustomFont';
    src: url('custom-font.woff2') format('woff2');
    unicode-range: U+000-5FF;
}

当然,你也可以使用一些工具预先将字体文件进行裁剪,以减小字体体积。

图标字体(例如 Material Symbols)是比较特殊的。如果我们选择在它们加载完成前不会显示一串字母,那么我们可以先让所有图标隐藏,然后用 JavaScript 在字体加载完成后显示图标:

<i class="material-icons">face</i>
.material-icons {
    font-family: 'Material Icons';
    display: none;
}
document.fonts.ready.then(() => {
    document.querySelectorAll('.material-icons').forEach(icon => {
        icon.style.display = 'inline';
    });
});

脚本优化

脚本文件的加载速度也会影响网页加载速度。我们可以使用 defer 属性延迟脚本加载。defer 属性告诉浏览器立即下载脚本,同时继续解析文档,并在文档解析完成后执行脚本:

<script src="script.js" defer></script>

如果脚本与文档解析无关,我们可以使用 async 属性异步加载脚本。async 属性告诉浏览器立即下载脚本,同时继续解析文档。脚本会在加载完成后立即执行,执行可能发生在文档解析的任何时刻:

<script src="script.js" async></script>

第三方资源优化

在加载第三方资源时,浏览器大概率需要再次进行 DNS 查询。DNS 查询是一个耗时的操作,会影响网页加载速度。我们可以使用 preconnectdns-prefetch 预连接第三方资源。preconnect 告诉浏览器建立连接到指定 URL 的连接。dns-prefetch 告诉浏览器预解析指定 URL 的 DNS:

<link rel="preconnect" href="https://example.com">
<link rel="dns-prefetch" href="https://example.com">

但是,最好的方法还是将第三方资源下载到本地,在 CDN 缓存后再引用,这样可以进一步减少 DNS 查询。

HTML 优化

流行的打包工具(例如 Webpack、Vite)都支持压缩资源。我们可以使用这些工具将资源压缩成更小的体积:删除空格、注释、无用代码;压缩 JavaScript、CSS、HTML 文件。而一些服务提供商(例如 Cloudflare)也提供了资源压缩服务。我们可以在 CDN 缓存时压缩资源,以减小资源体积,提升网页加载速度。

此外,我们最好能够减少 DOM 节点。DOM 节点越多,浏览器渲染页面的时间就越长。我们可以使用一些技巧减少 DOM 节点,例如使用文档片段、使用 innerHTML 替代 appendChild、使用事件委托等。

预判

有时候,我们需要一些怪异的优化手段。例如,使用 prerender 预渲染页面。prerender 属性告诉浏览器预渲染用户可能会跳转到的页面:

<link rel="prerender" href="https://example.com">

这么做会让浏览器在后台渲染页面,当用户跳转到这个页面时,页面会立即显示。但是,这样会增加服务器负担。

但是,如果用户大概率会跳转到这个页面,那么这么做是值得的。

结语

网页加载速度优化其实并不复杂,主要就是将紧迫性不高的资源延迟加载,将不必要的资源删除,将大资源压缩,将网络请求减少。

当然,有时候极端的优化会让用户体验很糟糕,比如先加载进了 HTML 再慢慢加载 CSS,会让用户看到一堆没有样式的内容。所以,优化的目的是提升用户体验,而不是追求极致的加载速度。

Comments

Share This Post