0. 目的
我的一个.net 9 webapp, 使用了bootstrap v5.3, 在发布后google搜索认为性能不够好,其他的都可以优化到100分,就这个LCP因为boostrap.mini.css, 在移动设备上的得分到85分后就提高不上去了,应用preload等等方法效果都不好。
我尝试用unused-css.com在线优化,发现可以减小boostrap.mini.css约2/3,但这个网站是收费的,放弃。
然后去问了下Grok AI,建议用vite进行tree-shaking,去掉没有使用到的部分。
以下方案可以将
未压缩 | br压缩 | gzip | vite tree-shaking后 | lighthouse得分 | |
bootstrap.mini.css | 224k | 23k | 31k |
三项合并:未压缩:58k, BR:10k, Gzip:12k |
95 |
site.css, | 3k | 1k | 2k | ||
BKTBlog.styles.css | 2k | 1k | 1k |
在 .NET 9 Web 应用程序中,Tree-shaking(摇树优化)通常用于移除未使用的 CSS 代码,以减少最终打包的文件大小并提升页面加载性能。以下是实现 CSS Tree-shaking 的具体步骤和方法:
1. 理解 Tree-shaking 的前提
Tree-shaking 是一种优化技术,主要依赖于模块化的代码结构和静态分析工具。CSS Tree-shaking 的目标是分析哪些 CSS 规则在应用程序中实际被使用,并移除未使用的样式规则。以下工具和环境是关键:
-
模块化 CSS:使用 CSS 模块、PostCSS 或其他模块化 CSS 工具。
-
构建工具:如 Webpack、Vite 或 esbuild,用于静态分析和打包。
-
PurgeCSS 或类似工具:专门用于移除未使用的 CSS。
在 .NET 9 的 Web 应用程序中,通常使用 ASP.NET Core 的前端工具链(如 Blazor 或 MVC)结合现代构建工具来实现 CSS Tree-shaking。
2. 配置项目环境
假设你正在使用 ASP.NET Core 9 项目,以下是配置步骤:
a. 确保使用现代前端构建工具
ASP.NET Core 项目默认不包含前端构建工具,你需要手动集成 Webpack、Vite 或其他工具。以下以 Vite 为例,因为它简单且支持 Tree-shaking。
-
安装 Node.js 和 npm: 确保你的开发环境中已安装 Node.js(推荐版本 18 或以上)。
-
初始化前端项目: 在你的 ASP.NET Core 项目根目录下,运行以下命令初始化一个前端项目:
npm init -y npm install vite --save-dev
-
创建 Vite 配置文件: 在项目根目录下创建 vite.config.js:
import { defineConfig } from 'vite'; //import postcss from 'vite-plugin-postcss'; import fs from 'fs'; import path from 'path'; export default defineConfig({ //plugins: [postcss()], base: './', // 设置基础路径为相对路径 build: { outDir: 'dist', // 输出到 ASP.NET Core 的静态文件目录 assetsDir: 'assets', manifest: true, // 添加这一行来生成 manifest.json minify: 'esbuild', // 使用 esbuild 进行压缩 rollupOptions: { input: Object.fromEntries( fs.readdirSync('.') .filter(file => file.endsWith('.html')) .map(file => [path.parse(file).name, file]) ), }, }, });
-
安装 PostCSS 和 PurgeCSS: PurgeCSS 是实现 CSS Tree-shaking 的核心工具。运行以下命令:
npm install --save-dev postcss @fullhuman/postcss-purgecss
3. 配置 PurgeCSS 进行 CSS Tree-shaking
PurgeCSS 会扫描你的 HTML、Razor 视图、JavaScript 和其他文件,分析哪些 CSS 选择器被使用,并移除未使用的规则。
3.1. 创建 PostCSS 配置文件
在项目根目录下创建 postcss.config.js:
import purgecss from '@fullhuman/postcss-purgecss';
export default {
plugins: [
// 尝试 purgecss.default,如果不存在则回退到 purgecss
// 这有助于处理 ESM/CJS 互操作可能导致的问题
(purgecss.default || purgecss)({
content: [
'./**/*.cshtml', // 扫描 Razor 视图
'./**/*.html', // 扫描 HTML 文件
'./*.html', // 扫描单个 HTML 文件
'./**/*.js', // 扫描 JavaScript 文件
//'./**/*.ts', // 如果使用 TypeScript
//'./**/*.razor', // 如果使用 Blazor
],
defaultExtractor: (content) => content.match(/[\w-/:]+(?<!:)/g) || [],
safelist: ['html', 'body'], // 保留某些选择器
}),
]
};
-
content:指定需要扫描的文件路径,确保包含你的 Razor 视图(.cshtml)、Blazor 组件(.razor)等。
-
defaultExtractor:定义如何提取 CSS 选择器。
-
safelist:列出需要保留的 CSS 选择器,防止误删(如 html 和 body)。
3.2. 组织 CSS 文件
将你的 CSS 文件模块化,例如使用 CSS 模块或单独的 .css 文件。假设项目结构如下:
MyWebApp/
├── wwwroot/
│ ├── css/
│ │ └── app.css
├── Pages/
│ └── Index.cshtml
├── vite.config.js
├── postcss.config.js
├── package.json
在 wwwroot/css/app.css 中编写你的样式:
/* app.css */
.container {
background-color: #f0f0f0;
}
.unused-class {
color: red;
}
在 Pages/Index.cshtml 中引用 CSS:
<link rel="stylesheet" href="/css/app.css" />
<div class="container">
<h1>Hello, World!</h1>
</div>
4. 构建和运行
4.1 添加构建脚本: 在 package.json 中添加以下脚本:
"scripts": {
"build": "vite build",
"dev": "vite"
}
最终的package.json文件:
{
"name": "vite",
"version": "1.0.0",
"main": "index.js",
"type": "module",
"scripts": {
"build": "vite build",
"dev": "vite"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"devDependencies": {
"@fullhuman/postcss-purgecss": "^7.0.2",
"vite": "^6.3.5"
}
}
4.2 运行构建: 执行以下命令,Vite 会使用 PurgeCSS 分析并移除未使用的 CSS:
npm run build
构建完成后,检查 /dist/assets 目录下的输出 CSS 文件。你会发现未使用的 .unused-class 规则已被移除。
4.3 获取.css路径
前面在vite.config.js中添加了manifest: true, 用于生成 .vite/manifest.json,类似如下内容
"_BKTBlog.yYDNlSlB.css": {
"file": "assets/BKTBlog.yYDNlSlB.css",
"src": "_BKTBlog.yYDNlSlB.css"
},
这里的 "assets/BKTBlog.yYDNlSlB.css"
是包含有hash值的css文件名,hash值每次编译都变化。
现在只要在编译后,获取到这个值,然后写入到siteconfig.xml, 这个文件记录了站点的配置信息,写入这个文件,就可以在.net 9 WebApp中获取到,应用到Layout中。
下面这个update-siteconfig.js,内容如下,就是完成css文件名获取,并写入到sitecofnig.xml中。
import fs from 'fs/promises';
import path from 'path';
import xml2js from 'xml2js';
async function updateSiteConfig() {
// Vite 输出目录和 manifest 文件路径
const manifestPath = path.resolve('dist', '.vite','manifest.json');
// siteconfig.xml 文件路径
const siteConfigPath = path.resolve('siteconfig.xml');
try {
// 1. 读取 manifest.json
const manifestContent = await fs.readFile(manifestPath, 'utf-8');
const manifest = JSON.parse(manifestContent);
// 2. 查找目标 CSS 文件
// 根据你的 vite.config.js, iconfont-razor.css 会被打包成 assets/BKTBlog.[hash].css
// 我们将查找这个模式的文件
let targetCssPath = null;
for (const key in manifest) {
const asset = manifest[key];
// 检查 asset.file (主要输出文件)
if (asset.file && typeof asset.file === 'string' &&
asset.file.startsWith('assets/BKTBlog.') && asset.file.endsWith('.css')) {
targetCssPath = asset.file;
break;
}
// 检查 asset.css (如果 CSS 是通过 JS chunk 引入的)
if (asset.css && Array.isArray(asset.css)) {
for (const cssFile of asset.css) {
if (typeof cssFile === 'string' && cssFile.startsWith('assets/BKTBlog.') && cssFile.endsWith('.css')) {
targetCssPath = cssFile;
break;
}
}
}
if (targetCssPath) break;
}
if (!targetCssPath) {
console.error('错误:在 dist/manifest.json 中未找到目标 CSS 文件 (assets/BKTBlog.*.css)。');
process.exit(1);
}
console.log(`找到目标 CSS: ${targetCssPath}`);
// 3. 读取和解析 siteconfig.xml
const siteConfigContent = await fs.readFile(siteConfigPath, 'utf-8');
const parser = new xml2js.Parser();
const builder = new xml2js.Builder({ xmldec: { 'version': '1.0', 'encoding': 'utf-8' } });
const result = await parser.parseStringPromise(siteConfigContent);
// 4. 更新 SiteMiniCss 的值
if (result.SiteConfig && result.SiteConfig.SiteMiniCss && result.SiteConfig.SiteMiniCss[0] !== undefined) {
result.SiteConfig.SiteMiniCss[0] = targetCssPath;
} else {
console.error('错误:在 siteconfig.xml 中未找到 SiteConfig.SiteMiniCss 元素。');
process.exit(1);
}
// 5. 写回更新后的 XML 内容
const updatedXml = builder.buildObject(result);
await fs.writeFile(siteConfigPath, updatedXml, 'utf-8');
console.log(`成功更新 ${siteConfigPath},SiteMiniCss 设置为: ${targetCssPath}`);
} catch (error) {
console.error('更新 siteconfig.xml 时发生错误:', error);
process.exit(1);
}
}
updateSiteConfig();
package.json文件修改下,增加:&& node ./update-siteconfig.js
"scripts": {
"build": "vite build && node ./update-siteconfig.js",
"dev": "vite"
},
这样执行npm run build构建后,就会自动将新的css文件名写入siteconfig.xml中。
在 ASP.NET Core 中引用构建结果: 修改 Index.cshtml 或布局文件(如 _Layout.cshtml),引用构建后的 CSS:
<link rel="stylesheet" href="@siteConfig.SiteMiniCss" />
xml文件是站点配置类序列化后的结果,每次启动web app时从文件读取完成初始化。然后注入,就可以使用了。
5. 集成到 ASP.NET Core 构建流程
为了在开发和发布时自动运行 Vite 构建,可以将 Vite 集成到 ASP.NET Core 的 MSBuild 流程中。
a. 修改 .csproj 文件
编辑你的 ASP.NET Core 项目文件(MyWebApp.csproj),添加以下内容:
<Target Name="RunViteBuild" BeforeTargets="Build">
<Exec Command="npm run build" WorkingDirectory="$(ProjectDir)" Condition="'$(Configuration)' == 'Release'" />
</Target>
这会在发布构建(Release 模式)时自动运行 npm run build。
b. 开发模式
在开发模式下,可以运行 Vite 的开发服务器:
npm run dev
Vite 会启动一个热重载服务器,监听 CSS 和其他文件的变化。确保在 Index.cshtml 或 _Layout.cshtml 中引用 Vite 的开发服务器地址:
<link rel="stylesheet" href="http://localhost:5173/css/app.css" />
6. 注意事项
-
动态类名:如果你的项目中使用动态生成的类名(如 Tailwind CSS 或 JavaScript 动态添加类),确保 PurgeCSS 能正确识别。可以调整 safelist 或使用 PurgeCSS 的 dynamicAttributes 选项。
-
Blazor 特定问题:Blazor 组件的 CSS 隔离会生成特定的选择器(如 b-xxx),确保 PurgeCSS 配置文件中包含 .razor 文件,并测试 Tree-shaking 结果。
-
性能优化:对于大型项目,PurgeCSS 可能会增加构建时间。考虑在开发模式下禁用 PurgeCSS,仅在生产构建时启用:
module.exports = { plugins: [ process.env.NODE_ENV === 'production' ? require('@fullhuman/postcss-purgecss')(/* 配置 */) : null, ].filter(Boolean), };
7. 验证 Tree-shaking 效果
-
检查构建后的 CSS 文件大小是否显著减小。
-
使用浏览器的开发者工具(F12),检查是否有多余的 CSS 规则加载。
-
如果使用 Tailwind CSS,可以结合 Tailwind 的 JIT 模式和 PurgeCSS,进一步减少 CSS 体积。
示例:Tailwind CSS + PurgeCSS
如果你的项目使用 Tailwind CSS,可以直接在 postcss.config.js 中配置 PurgeCSS:
module.exports = {
plugins: [
require('tailwindcss'),
require('@fullhuman/postcss-purgecss')({
content: ['./**/*.cshtml', './**/*.razor', './**/*.html', './**/*.js'],
defaultExtractor: (content) => content.match(/[\w-/:]+(?<!:)/g) || [],
}),
],
};
在 tailwind.config.js 中,确保 PurgeCSS 能扫描正确的文件:
module.exports = {
content: ['./**/*.cshtml', './**/*.razor', './**/*.html', './**/*.js'],
theme: {
extend: {},
},
plugins: [],
};
总结
通过以上步骤,你可以在 .NET 9 Web 应用程序中实现 CSS Tree-shaking:
-
使用 Vite 或 Webpack 作为构建工具。
-
集成 PurgeCSS,配置扫描路径和保留规则。
-
将构建流程集成到 ASP.NET Core 项目中。
-
测试和验证 Tree-shaking 效果。
如果你的项目规模较大或有特殊需求(如动态类名),可以进一步优化 PurgeCSS 配置或探索其他工具(如 UnCSS)。如果需要更详细的代码示例或特定问题解决,请提供更多细节,我可以进一步协助!