作者:匿名
发布:3天前

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。
  1. 安装 Node.js 和 npm: 确保你的开发环境中已安装 Node.js(推荐版本 18 或以上)。
  2. 初始化前端项目: 在你的 ASP.NET Core 项目根目录下,运行以下命令初始化一个前端项目:
    npm init -y
    npm install vite --save-dev
  3. 创建 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])
          ),
        },
      },
    });
  4. 安装 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:
  1. 使用 Vite 或 Webpack 作为构建工具。
  2. 集成 PurgeCSS,配置扫描路径和保留规则。
  3. 将构建流程集成到 ASP.NET Core 项目中。
  4. 测试和验证 Tree-shaking 效果。
如果你的项目规模较大或有特殊需求(如动态类名),可以进一步优化 PurgeCSS 配置或探索其他工具(如 UnCSS)。如果需要更详细的代码示例或特定问题解决,请提供更多细节,我可以进一步协助!
Tags: Dotnet   tree-shaking   vike  
1天前
30
1
0