font-src安全策略配置踩坑记:从CSP报错到完美解决的实战经验
优化前:卡得不行
最近接手一个老项目,用户反馈页面加载特别慢,尤其是首页。Chrome DevTools 显示首屏渲染要6-8秒,其中字体资源占了很大一部分时间。优化前的状态就是,页面内容都出来了,但是字体还没加载完,看着就像网页出bug了一样,全是方框和默认字体,用户体验差到爆。
问题主要出在自定义字体上,项目里用了几个WOFF2格式的中文字体文件,加起来有好几MB。而且这些字体都是从CDN加载的,网络请求阻塞了页面渲染,还经常出现FOIT(Flash of Invisible Text)问题。
找到瓶颈了!
用Lighthouse跑了一下性能测试,发现字体加载时间平均在2-3秒,有些用户的网络环境更差。Performance面板显示,CSS字体声明导致了渲染阻塞,字体加载完成前页面空白或者显示默认字体。
通过Network面板进一步分析,发现font-src配置不当,允许了过多的字体来源,而且没有做预加载优化。Security面板也提示CSP策略太宽松,存在安全风险的同时也影响了性能。
字体加载优化的核心代码
首先是对字体资源的优化,这是最核心的部分。我采用了多种策略组合:
<!-- 预连接,提前建立DNS连接 -->
<link rel="preconnect" href="https://jztheme.com/fonts/" crossorigin>
<!-- 字体预加载 -->
<link rel="preload" href="/fonts/custom-font.woff2" as="font" type="font/woff2" crossorigin>
<style>
/* 优化前的低效写法 */
@font-face {
font-family: 'CustomFont';
src: url('/fonts/custom-font.woff2') format('woff2');
font-display: swap; /* 这个其实已经不错了 */
}
/* 优化后的高效写法 */
@font-face {
font-family: 'CustomFont';
src: url('/fonts/custom-font-subset.woff2') format('woff2');
font-display: optional; /* 关键优化点 */
font-weight: 400;
font-style: normal;
}
</style>
这里的重点是 font-display: optional,比之前的 swap 更激进一些。它会在字体加载失败或超时时完全回退到系统字体,避免长时间等待。不过要注意,这种设置对品牌字体可能不太友好。
针对font-src的CSP策略我也做了收紧:
// 优化前的宽松策略
// Content-Security-Policy: font-src *;
// 优化后的精准策略
const cspHeader = [
"default-src 'self'",
"font-src 'self' https://jztheme.com/fonts/ https://fonts.googleapis.com",
"style-src 'self' 'unsafe-inline'",
"script-src 'self'"
].join('; ');
app.use((req, res, next) => {
res.setHeader('Content-Security-Policy', cspHeader);
next();
});
字体子集化处理
这个优化带来了最大收益。原来完整的中文字体文件有2MB多,加载时间感人。通过字体子集化,只保留项目中实际使用的字符,体积缩小到200KB左右。
我是用fonttools来处理的,命令如下:
# 提取文本中使用的字符
pyftsubset custom-font.ttf --text-file=used-chars.txt --output-file=custom-font-subset.woff2 --flavor=woff2
用Python脚本扫描项目中的文本内容,生成实际使用的字符列表:
import os
import re
def extract_chars_from_html(file_path):
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# 提取所有中文字符
chars = set(re.findall(r'[u4e00-u9fff]', content))
return chars
# 扫描项目文件
all_chars = set()
for root, dirs, files in os.walk('./public'):
for file in files:
if file.endswith('.html'):
all_chars.update(extract_chars_from_html(os.path.join(root, file)))
with open('used-chars.txt', 'w', encoding='utf-8') as f:
f.write(''.join(all_chars))
异步加载策略
对于非关键字体,我采用异步加载方式:
function loadFontAsync(fontUrl, fontName) {
const link = document.createElement('link');
link.rel = 'preload';
link.href = fontUrl;
link.as = 'font';
link.type = 'font/woff2';
link.crossOrigin = 'anonymous';
link.onload = () => {
// 字体加载完成后应用
document.documentElement.style.setProperty('--loaded-font', fontName);
};
document.head.appendChild(link);
}
// 页面加载后再加载装饰性字体
window.addEventListener('load', () => {
loadFontAsync('/fonts/decoration-font.woff2', 'DecorationFont');
});
性能数据对比
优化前后的数据对比很直观:
- FIRST CONTENTFUL PAINT: 从4.2s缩短到1.8s
- LARGEST CONTENTFUL PAINT: 从6.8s缩短到2.1s
- FONTS LOAD TIME: 从2.8s降低到0.6s
- PAGE WEIGHT: 减少了1.8MB
Lighthouse分数也有明显提升,移动端从65分提高到88分。用户反馈页面响应速度快了很多,不会再出现长时间白屏的情况。
缓存策略调整
配合font-src优化,我也重新设计了字体缓存策略:
# Nginx配置
location ~* .(woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
add_header Access-Control-Allow-Origin "*";
}
字体文件基本不会变,所以设置了1年的强缓存。这样用户第二次访问时几乎不需要重新下载字体。
一些踩坑记录
过程中也踩了不少坑。最开始我想用system font stack来替代自定义字体,但产品方坚持要用品牌字体,所以这条路走不通。
还有一次在测试font-display的时候,设成了block,结果在慢网速下页面卡死了5秒多才显示文字,体验还不如原来的swap。后来改成optional才解决了这个问题。
CSP配置也折腾了很久,刚开始把font-src限制得太死,导致第三方组件的字体加载失败。经过多次调试才找到平衡点。
监控和后续
部署后我加了字体加载的监控:
// 监控字体加载性能
if ('fonts' in document) {
document.fonts.ready.then(() => {
performance.mark('fonts-loaded');
const perfEntries = performance.getEntriesByName('fonts-loaded');
console.log('Font loading time:', perfEntries[0].startTime);
});
}
目前线上运行稳定,字体相关的性能指标保持良好。不过字体子集化的维护成本有点高,每次新增文案都需要重新生成字体文件,这部分还需要自动化流程支持。
以上是我个人对font-src性能优化的完整讲解,有更优的实现方式欢迎评论区交流。

暂无评论