vite项目首屏渲染优化实战:从10.5s到1.5s的优化实录

934 阅读5分钟

一、性能问题分析

1.1 问题背景

某用vite构建的SPA系统在实现离线大文件的下载功能后,多个页面出现首屏加载缓慢问题:

  • 使用该文件的页面:首次加载10-15秒
  • 连带影响:未使用该功能的页面也受到波及

特别影响用户体验感和开发速度。

1.2 分析工具详解

(1)VITE项目-Rollup打包分析(rollup-plugin-visualizer)

(上图来自github)

github地址:rollup-plugin-visualizer

简介ℹ️:vite插件,可视化的打包依赖分析工具。通过分析依赖模块的大小占比,可以让我们更有针对性的进行体积优化。

配置示例

// vite.config.ts
import { visualizer } from 'rollup-plugin-visualizer';

export default {
  plugins: [
    // visualizer构建分析,注意放在最后面,打包完成后分析
    visualizer({
      open: true,  // 自动打开
      gzipSize: true,
      template: 'treemap' // 树状图模式
      filename: 'build-stats.html',
    })
  ]
}

使用场景

  • 识别打包产物中的巨型模块
  • 对比优化前后的体积变化

(2)Chrome DevTools实战技巧

Network面板进阶用法

1. 切换至All查看所有资源请求情况
2. Size,排序资源,定位体积较大的文件
3. Time, 点击顶部排序可找出加载时间较长的部分
4. Initiator,可进一步查看文件引入路径

image.png

Coverage工具

1. Ctrl+Shift+P -> Show Coverage
2. 刷新页面获取代码使用率
3. 红色标注未使用代码段

image.png

image.png


二、优化方案实现

2.1 静态资源压缩(核心方案)

(1)依赖清理

原理:Tree Shaking + 按需加载
实现

// 原始代码(全量引入)
import { HeavyLib } from 'univerjs';

// 优化代码(按需加载)
import { SheetRenderer } from 'univerjs/core';

效果:移除univerjs引入,体积减少10MB+

(2)i18n词条过滤

实现原理
通过构建时脚本过滤仅保留相关词条

关键代码

// filter-i18n.js
const prefix = 'report.';
const filtered = Object.keys(fullLang)
  .filter(k => k.startsWith(prefix))
  .reduce((acc, k) => (acc[k] = fullLang[k], acc), {});

fs.writeFileSync('report-lang.json', JSON.stringify(filtered));

构建命令

# 自定义构建脚本
pnpm update:my-i18n

(3)字体优化

实现方案

/* 移除定制字体 */
/* 原代码:@font-face { font-family: CustomFont; ... } */

/* 使用系统字体 */
body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui;
}

antd可结合ConfigProvider进行局部配置,以免影响整体的字体。

核心代码

import React from 'react'; 
import { ConfigProvider } from 'antd'; 
const App: React.FC = () => (
<ConfigProvider 
theme={{ token: { fontFamily:'-apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial,Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol,Noto Color Emoji' } }}> 
<MyApp /> 
</ConfigProvider>
); 

export default App;

(4)vite-build.rollupOptions.external 移除不用的依赖

结合vite配置中的配置项,可以主动排除不想编译的比较大的依赖和文件;不过仅能是排除真的不需要用的部分。

2.2 动态加载策略, 关键渲染路径优化

(1)React异步组件: 懒加载+代码分割

实现原理:代码分割 + 请求延迟((React.lazy + Suspense))

关键代码

const ReportViewer = React.lazy(() => import('./ReportViewer'));

function App() {
  return (
    <Suspense fallback={<Spin />}>
      <ReportViewer />
    </Suspense>
  );
}

优化效果 有大依赖项的菜单,补充设置懒加载,各路由按需引入;index.js主入口文件体积缩小4.9m。

(2)useSuspenseQuery + import(),实现大模块局部延迟加载

实现原理:等首屏加载完成后灵活控制加载时机,再异步加载大模块;

  • 页面渲染后,react-query 会在后台异步加载大模块
  • 不会阻塞首屏渲染
  • 灵活控制加载时机(比如打开弹窗时、页面空闲时、用户滑动到某区域时等)
  • 结合 Suspense,loading体验更好

关键代码

import { useSuspenseQuery } from '@tanstack/react-query'
import { t } from 'i18next'
import { Suspense } from 'react'

async function loadDownloadBtn() {
  const mod = await import('@/pages/xxxx')
  return mod.default
}

function useDownloadBtn() {
  return useSuspenseQuery({
    queryKey: ['DownloadBtn'],
    queryFn: loadDownloadBtn,
    refetchOnMount: false,
  }).data
}

function DownloadBtnWrapper(props) {
  const DownloadBtn = useDownloadBtn()

  return <DownloadBtn {...props} />
}

export function DownloadBtnSuspenseWrapper(props: any) {
  return (
    <Suspense
      fallback={
        <span className='cursor-not-allowed text-disabled-text-gray'>
          {t('global.downloadLoadingMsg')}
        </span>
      }
    >
      <DownloadBtnWrapper {...props} />
    </Suspense>
  )
}

(3)用fetch方法直接从public目录获取静态大文件📃

实现原理

  • 不需要React组件生命周期
  • 灵活控制加载时机,实现类似于发请求从后端获取的效果~
  • 另外,通过将大文件移入public目录下,以免整个项目build时被编译,以提升构建速度

核心代码

export const fetchStaticFileRaw = async () => {
  try {
    const response = await fetch('/bigFile.html?t=' + t)
    const htmlStr = await response.text()
    return htmlStr
  } catch (error) {
    message.error('Failed to fetch big file')
    return ''
  }
}

三、优化效果展示

image.png

image.png

指标优化前优化后提升幅度
大文件体积39.7MB3.76MB↓35.94MB (90.5%)
首屏JS体积41MB6.7MB↓34.52MB (83.6%)
LCP时间10.47s1.53s↓8.94s (85.4%)
资源加载峰值>5s<1s↓4.1s (82%)

四、编码思考与经验沉淀

  • 关键优化手段

    • 精准依赖管理:通过静态分析剔除冗余代码(如UniverJS、Redux),vite结合按需加载(i18n词条过滤)减少死代码。
    • 动态加载策略:利用React.lazy + Suspense + React-Query / fetch方法实现按需加载,避免阻塞关键渲染路径。
    • 工程化定制:通过Vite插件链(如rollup-plugin-visualizer)和自定义脚本(词条过滤命令行工具)实现自动化优化。
  • “代价与收益”的权衡

    • 动态加载可能增加代码复杂度,需通过封装(如DownloadBtnSuspenseWrapper)保证可维护性。
  • 依赖治理的重要性

    • 第三方库(如UniverJS)的引入需严格评估必要性,避免“大而全”的依赖污染打包结果。
    • 建立代码体积监控机制(如定期运行visualizer),防止体积劣化。
  • 跨角色协作的价值

    • 与UX设计师和产品沟通替换字体资源,验证设计可行性。

性能优化不是一次性任务,而是持续改进的过程。建议将本文方案融入日常开发流程,通过工具化和自动化实现长效治理。