Design System从零搭建实战踩坑记
优化前:卡得不行
最近重构公司内部的设计系统,之前的版本用起来真的卡得不行。组件库体积超过3MB,页面加载时间动不动就5-6秒起步,有时候还要翻倍。用户反馈特别差,每次打开设计规范页面都得等半天,根本没法正常浏览。更要命的是,组件切换的时候明显感觉到延迟,特别是复杂的表单组件,点击交互响应慢半拍。
我接手这个项目的时候,第一反应就是这性能必须得优化。不然这么大的组件库,用户怎么可能用得下去。当时的bundle分析显示,光是核心的UI组件就占了1.8MB,加上依赖的各种图标字体、样式文件,整个包体确实太大了。
找到瓶颈了!
用webpack-bundle-analyzer分析了一下,发现问题主要集中在几个地方:一是所有组件一次性全部打包,二是样式文件没有按需加载,三是有很多重复的依赖包。Chrome DevTools显示,首屏渲染时间超过4秒,LCP(最大内容绘制)更是惨不忍睹。
还发现一个问题,就是组件的按需加载机制写得很粗糙,虽然表面上支持按需引入,但实际上还是会把整个样式包一起引入。这个问题折磨了我好几天,用各种调试工具一层层扒才发现,原来是在组件的index.js里偷偷引入了全局样式。
按需加载改造
首先干掉那个大而全的样式文件。原来的写法是这样的:
// 优化前的组件入口
import React from 'react';
import './styles/index.css'; // 这个是全量样式
import Button from './Button';
import Input from './Input';
import Modal from './Modal';
export { Button, Input, Modal };
改成真正的按需加载:
// 优化后的组件入口
import React from 'react';
// 不再引入全量样式
import Button from './Button/Button';
import Input from './Input/Input';
import Modal from './Modal/Modal';
export { Button, Input, Modal };
同时修改构建配置,让每个组件都有独立的样式文件:
// webpack配置
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\/]node_modules[\/]/,
name: 'vendors',
chunks: 'all',
},
styles: {
name: 'styles',
type: 'css/mini-extract',
chunks: 'all',
enforce: true,
}
}
}
}
};
动态导入 + 预加载
这里踩了个坑,动态导入的时候要配合预加载策略。不然用户体验会很糟糕,点击某个组件才开始加载对应的模块,那等待时间真的很尴尬。
// 使用React.lazy进行懒加载
import React, { lazy, Suspense } from 'react';
const HeavyComponent = lazy(() =>
import('./components/HeavyComponent')
);
// 加入预加载策略
function ComponentWithPreload() {
const [show, setShow] = useState(false);
useEffect(() => {
if (show) {
// 预加载可能用到的组件
import('./components/HeavyComponent');
}
}, [show]);
return (
<Suspense fallback={Loading...}>
{show && }
);
}
不过预加载也不是随便乱用,要考虑用户的使用路径。比如用户进入表单页面时,大概率会用到各种输入组件,这时候提前加载是有意义的。但如果用户只是浏览文档,那就没必要加载所有组件了。
CSS优化重头戏
这部分花了最多时间,因为要彻底解决样式冗余的问题。原来的方案是每个组件都引入一套完整的CSS变量和混合器,导致重复代码很多。
优化后的架构:
/* base.css - 基础变量和混合器 */
:root {
--color-primary: #007bff;
--border-radius: 4px;
}
@mixin button-base() {
border: none;
cursor: pointer;
}
/* button.css - 按钮专属样式 */
.btn {
@include button-base();
background: var(--color-primary);
border-radius: var(--border-radius);
}
这样做的好处是,如果只用按钮组件,就不会加载表格、弹窗等其他组件的样式代码。配合PostCSS的purge插件,能进一步减少CSS体积。
性能数据对比
优化完成后的数据变化还是很明显的:
- Bundle体积:3.2MB → 850KB(减少了73%)
- 首屏加载时间:5.2秒 → 1.1秒
- LCP时间:5.8秒 → 1.3秒
- 交互响应时间:平均800ms → 平均150ms
这些数字看起来很美好,但其实过程中踩了不少坑。比如动态导入的时机控制,一开始想得太过理想化,结果发现预加载策略不对反而会拖慢速度。还有样式隔离问题,多个组件共享样式变量时容易出现冲突,最后通过CSS模块化解决了。
缓存策略也很关键
除了代码分割,CDN缓存策略也重新设计了。静态资源设置长期缓存,版本更新时通过hash值区分:
module.exports = {
output: {
filename: '[name].[contenthash].js',
chunkFilename: '[name].[contenthash].chunk.js',
},
module: {
rules: [
{
test: /.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader'
]
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].[contenthash].css',
})
]
};
服务端渲染优化
为了更好的SEO和首屏体验,还加了SSR支持。这里主要考虑的是样式提取,避免FOUC(Flash of Unstyled Content)问题。
app.get('*', (req, res) => {
const html = ReactDOMServer.renderToString();
const css = extractStyles(); // 提取关键CSS
res.send(
<!DOCTYPE html>
<html>
<head>
<style>${css}</style>
</head>
<body>
<div id="root">${html}</div>
<script src="/client.js"></script>
</body>
</html>
);
});
总结一下
这套Design System的性能优化,前后花了差不多三周时间。最大的收获是明白了按需加载的精髓不在技术本身,而在于对用户使用场景的理解。盲目地拆分模块还不如不分,一定要结合实际的使用路径来做优化。
另外,构建工具的配置真的很重要,之前一直觉得webpack那些配置很复杂不想碰,这次深度定制后发现收益很大。特别是splitChunks的配置,稍微调整一下就能带来明显的效果提升。
当然还有一些细节没提到,比如图标组件的SVG精灵优化、图片懒加载等等。这些小优化叠加起来,整体效果还是很不错的。
以上是我这次Design System性能优化的经验总结,有更优的实现方式欢迎评论区交流。

暂无评论