@import 是一条串行请求链 —— 用 Resource Timing 自己量,别听人说

样式表里的 @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。

← 返回技术笔记