link 和 @import 引入 CSS 有什么区别?
link 是 HTML 元素,浏览器解析 HTML 时就能发现并下载;@import 写在 CSS 里,要等外层样式表下载解析后才发现,形成串行的请求链,延迟 CSSOM 构建。link 能用 JavaScript 增删、切换 disabled,用 media 属性做条件加载;@import 必须写在样式表开头,支持媒体查询、supports() 条件和 layer() 放入层叠层,后者是 link 做不到的。
简答
引入外部 CSS 有两种写法:在 HTML 的
<head>里写<link rel="stylesheet" href="a.css">,或者在 CSS 文件开头写@import "a.css";。两者最终都让样式生效,区别在于浏览器什么时候知道要下载它。
link写在 HTML 里,浏览器解析 HTML 时就发现了,可以和页面里其他资源一起尽早下载。@import藏在 CSS 文件里,要等外层的 CSS 下载并解析后才发现,再去下载,这就形成了串行的请求链。Chrome 团队的性能建议明确说不要在 CSS 里用@import,因为它会推迟 CSSOM 的构建,而<head>里的样式表本身就会阻塞渲染。
link是 DOM 元素,可以用 JavaScript 创建、删除,或者切换disabled属性来启用、停用一份样式表,比如做主题切换;还能用media属性让打印样式之类的不阻塞渲染。@import是 CSS 规则,只能通过 CSSOM 里的CSSImportRule读取。
@import也有自己的优势:它可以带layer()把整份外部样式放进一个层叠层,link没有对应的属性;还可以带supports()和媒体查询,条件不满足时浏览器可以不下载。要注意@import必须写在样式表最前面,只能排在@charset和@layer声明之后,否则会被忽略。
两种写法的加载时机
先看两种写法:
<!-- index.html -->
<link rel="stylesheet" href="base.css" />
<link rel="stylesheet" href="theme.css" />
/* base.css */
@import "theme.css";
body { margin: 0; }
第一种写法里,浏览器读到 HTML 就知道要下载 base.css 和 theme.css,两个请求可以同时发出去。
第二种写法里,HTML 里只有 base.css。浏览器要先把 base.css 下载下来、开始解析,读到 @import 才知道还有 theme.css,这时才发出第二个请求。也就是说,第二个文件至少要多等一个来回。Chrome 团队的性能指南把这叫做“串行的请求链”,并建议不要在 CSS 里用 @import。
为什么这个延迟会影响首屏?MDN 写明,<head> 里 rel="stylesheet" 的 link 默认会阻塞渲染,load 事件要等样式表连同它导入的内容都加载解析完才触发。所以 @import 进来的文件越晚被发现,页面越晚开始渲染。
用构建工具时情况不同。比如 Vite 文档写明它通过 postcss-import 预先内联 CSS 里的 @import,浏览器拿到的是合并后的文件,就没有这个问题。上面说的串行请求,指的是浏览器直接加载带 @import 的 CSS 文件的情况。
在 JavaScript 里能做什么
link 是普通的 DOM 元素,可以随时插入、删除。MDN 提到,link 上的 disabled 属性如果在 HTML 里就写上,页面加载时不会下载这份样式表,等属性被去掉再按需加载;在 DOM 上设置 disabled,这份样式表会从 document.styleSheets 里移除。所以主题切换常常是准备几个 link,切换它们的 disabled。
link 还有 load 和 error 事件,可以知道样式表是否加载成功。
@import 是 CSS 规则,不在 DOM 里。通过 document.styleSheets[0].cssRules 能拿到一个 CSSImportRule 对象,读它的 href、media、layerName、supportsText 和导入进来的 styleSheet,但没有办法像操作元素那样启用、停用它。
条件加载:media 和 supports()
两种写法都能按媒体条件引入:
<link rel="stylesheet" href="print.css" media="print" />
@import "print.css" print;
@import "grid.css" supports(display: grid) screen and (width <= 400px);
MDN 对 @import 的说法是,浏览器不支持其中任何一个媒体查询时不加载这个资源;supports() 条件不满足时可能不下载,即使通过其他途径下载了也不会应用。supports() 是 @import 独有的,link 没有对应的属性。
link 的 media 属性常被用来减少阻塞渲染。Chrome 团队的指南建议给打印样式、只在桌面用的样式这类当前用不到的样式表加上 media 属性,让它们下载时不阻塞渲染。
@import 和层叠层
@import 可以直接把一份外部样式放进层叠层:
@layer reset, vendor, app;
@import url("normalize.css") layer(reset);
@import url("vendor.css") layer(vendor);
MDN 说明,导入的规则在层叠中的表现,就像直接写在 @import 所在的位置一样;加上 layer() 以后,它们属于指定的层。link 元素没有 layer 属性,MDN 的属性列表里也没有,想把第三方库放进某个层,要么用 @import ... layer(),要么用构建工具包一层。层叠层的用法见《CSS Modules、层叠层和自定义属性怎样帮助维护样式?》。
这里也看到了 @import 位置的限制。MDN 写明,@import 必须在样式表最前面,只能排在 @charset 和用来声明层的 @layer 语句之后,放在其他规则后面会被忽略。它也不能写在 @media 这类条件规则里面,条件要直接写在 @import 后面。
常见追问
Sass 里的 @import 和 CSS 的 @import 是一回事吗?
不是。Sass 的
@import在编译时把文件内容合并进来,浏览器根本看不到它,而且 Sass 已经推荐改用@use,见《Sass、Less 这类 CSS 预处理器解决了什么问题?原生 CSS 现在能替代它们吗?》。原生 CSS 的@import是浏览器运行时去下载另一份样式表。
动态插入的 link 会阻塞渲染吗?
默认不会。MDN 写明只有
<head>里的link可能阻塞渲染,解析 HTML 时发现的rel="stylesheet"默认阻塞;如果是用脚本动态加进去的,要另外设置blocking="render"才会阻塞渲染。