样式表里的 @import url("theme.css"); 和平级的 <link rel="stylesheet">,差别不在”哪个更快”,而在依赖关系:
/* a.css */
@import url("theme.css");
body { margin: 0; }
浏览器下载完 a.css、解析到这一行,才知道世界上还有 theme.css。这是一个串行的瀑布:第二个请求的开始时间,等于第一个请求的结束时间加上解析耗时。
换成两个 <link>,浏览器在解析 HTML 时就能同时发现两个资源,并行发出去(同域名受连接数限制,但至少不用等前一个解析完)。
自己验,三行
performance.getEntriesByType('resource') 里有每个资源的时序。关键字段:startTime(发现它的时间)、requestStart(真正发出请求)、responseEnd(收完)。单位毫秒,基准是 navigationStart。
performance.getEntriesByType('resource')
.filter(e => ['css', 'link'].includes(e.initiatorType))
.sort((a, b) => a.requestStart - b.requestStart)
.forEach(e => console.log(
(e.name.split('/').pop() || '').slice(0, 32).padEnd(34),
'发现', Math.round(e.startTime),
'发出', Math.round(e.requestStart),
'完成', Math.round(e.responseEnd)
));
initiatorType 是 css 的,就是被 CSS 文件(@import 或 url())引出来的;是 link 的,来自 <link> 标签。看 requestStart 的先后:并行加载的资源,requestStart 会挤在很小的一段区间里;串行的,后一个的 requestStart 等于前一个的 responseEnd 加上几毫秒到几十毫秒的解析时间。
这个数字是自己机器上跑出来的,比任何”据说 @import 会慢”的说法都可靠——你的服务器、你的 RTT、你的缓存,都会让它不一样。
两个容易被忽略的规则
@import 必须写在文件最前面。 放在任何规则之后都会被忽略。所以”我在样式末尾补了个 import 怎么没生效”不是浏览器抽风,是规范行为。
带 media 的 @import 不阻塞渲染,但也不等于异步加载。 @import url(x) print; 在屏幕环境下不阻塞,Chrome 仍可能预加载它。拿它当”异步加载 CSS”的方案,属于碰运气。
真想异步加载非关键 CSS
<link rel="stylesheet" href="non-critical.css" media="print" onload="this.media='all'">
<noscript><link rel="stylesheet" href="non-critical.css"></noscript>
media="print" 让浏览器不把它当渲染阻塞资源,onload 触发后把 media 改回 all,样式才应用。<noscript> 兜住禁用 JS 的情况。
代价是首屏会有一次可见的样式跳变。要不要用,取决于那个样式表多大、跳变有多明显——不是”最佳实践”三个字能决定的。
为什么还留着 rel="preload" 这个选项
<link rel="preload" as="style" href="b.css" onload="this.rel='stylesheet'">
也能工作,语义上更「声明式」。但老版本 Safari 对 rel=preload 的支持不全,会退化成不加载。要兼容到 Safari 15 之前的话,上面那个 media="print" 的写法更稳——虽然它看起来像是 hack。