跪拜 Guibai
← Back to the summary

How I Made Vite's First Paint 10x Slower by Trying to Optimize It

Before chunk splitting

cc408b32-db99-4d6f-966f-06e122d1e1e7.png

After chunk splitting

747c85da-4209-4730-8609-180454e7b22e.png

Some optimizations don't need to be done. Vite's default configuration is the best practice.

Foreword

Let me show you a real data comparison.

This is the first-screen loading situation of the same Vue3 + Vite project before and after modifying the chunk splitting configuration:

Before chunk splitting (Vite default configuration):

Name Status Type Initiator Size Time
index-712a6fc0.js 200 script login:16 340 kB 319 ms
index-37f01f92.css 200 stylesheet login:17 328 kB 327 ms
login-c327e251.js 200 script index-712a6fc0.js:5 9.7 kB 51 ms
login-9a94892b.css 200 stylesheet index-712a6fc0.js:5 1.2 kB 49 ms
login 200 document /login 1.0 kB 39 ms
1710832132bj.webp 200 webp login-9a94892b.css 69.4 kB 290 ms

Total first-screen time: ~800ms

After chunk splitting (manual manualChunks configuration):

Name Status Type Initiator Size Time
vendor-a32a4159.js 200 script login:17 1,968 kB 6.56 s
element-plus-ff15f869.css 200 stylesheet login:21 327 kB 507 ms
element-plus-00e56c40.js 200 script login:19 276 kB 584 ms
vue-ecosystem-ec9ba455.js 200 script login:18 102 kB 215 ms
vendor-501cf061.css 200 stylesheet login:20 15.1 kB 155 ms
index-4d3f05e7.js 200 script login:16 13.3 kB 84 ms

Total first-screen time: ~7.5 seconds

Same project, same network environment, just changed the manualChunks configuration in vite.config.ts, first-screen time went from under 1 second to 7.5 seconds, nearly 10 times slower.

That afternoon me:

image.png

This article is not about teaching you how to configure Vite, but about sharing a realization: Vite's default configuration is much smarter than you think. Before optimizing, first confirm that a problem actually exists.

What exactly did I do?

Let me reconstruct my naive operation at the time.

The project uses Vue3 + Vite, first-screen load was about 800ms, which is actually quite fast. But I always felt:

So I added this configuration in vite.config.ts:

ts

export default defineConfig({
  build: {
      cssCodeSplit: true,
      rollupOptions: {
        output: {
          manualChunks: (id) => {
            if (id.includes('node_modules')) {
              if (id.includes('element-plus')) {
                return 'element-plus'
              }
              if (id.includes('vue') || id.includes('pinia') || id.includes('vue-router')) {
                return 'vue-ecosystem'
              }
              return 'vendor'
            }
          }
        }
      }
    },
})

Looking at this configuration, I was even a bit smug:

Perfect! 😎

Then npm run build, deploy, refresh the page, open the Network panel...

The smile gradually froze.

The data tells me: what is "reverse optimization"

Comparing the two charts, the problem is actually obvious.

Before chunk splitting (fast)

Under Vite's default chunk splitting strategy, the resources looked like this:

Multiple files loaded in parallel, none waiting for the others, fully utilizing browser concurrent requests, first-screen completed in about 800ms.

After chunk splitting (slow)

After my manual configuration, the situation became:

A vendor file nearly 2MB, just downloading took over 6 seconds.

More critically, this huge vendor file blocked the loading of all subsequent resources. The element-plus, CSS, business code that followed all had to wait for this file to download, parse, and execute.

A 2MB monolith file became the single bottleneck for the entire first-screen.

Why did my "optimization" backfire?

After calming down, I analyzed three levels of reasons.

1. The browser is not a pipe; bigger is slower

My thinking at the time was simple: since the total size doesn't change, bundling into one file reduces requests, isn't that faster?

But the browser doesn't work that way.

For a 2MB JS file, the browser needs to:

  1. Download (6.56 seconds)
  2. Parse
  3. Compile
  4. Execute

During this process, the browser's main thread is completely occupied, the page cannot render, and the user can only stare at a white screen.

When split into multiple smaller files, the browser can:

Total size is the same, but the loading experience is worlds apart.

2. I destroyed the caching strategy with my own hands

I originally wanted to improve cache hit rate through chunk splitting, but instead I ruined it.

Before chunk splitting, under Vite's default strategy, each third-party dependency had its own chunk:

If element-plus gets upgraded one day, only the element-plus chunk cache becomes invalid, the rest remain unchanged.

But after my manual configuration:

text

vendor-a32a4159.js  ← contains vue + vue-router + pinia + axios

If axios releases a minor version, the entire 1,968 kB vendor cache becomes invalid, and users have to re-download nearly 2MB of files.

Originally wanted to optimize caching, but instead the cache hit rate dropped significantly.

3. Vite's default strategy is much smarter than me

Vite has a built-in splitVendorChunkPlugin, whose strategy is simple but effective:

ts

// Vite internal approximate logic
if (id.includes('node_modules')) {
  // Each third-party package becomes a separate chunk
  // Leverage browser caching
}

Each third-party dependency is an independent chunk, loaded in parallel, without interfering with each other.

This strategy has been validated by thousands of projects, and is the optimal solution in the vast majority of scenarios.

What should the correct optimization approach be?

This experience made me rethink "frontend optimization."

Step 1: Don't optimize based on intuition

My biggest mistake was: optimizing "by feeling" without a performance problem.

The correct optimization process should be:

text

User feedback/monitoring alerts → Lighthouse/Performance measurement → Locate bottleneck → Targeted optimization → Re-measure and compare

Instead of:

text

I feel this can be optimized → Change configuration → It got slower → Confused

Step 2: Let data speak

Before optimizing, ask yourself a few questions:

Optimization without data support is likely just wishful thinking.

Step 3: When is custom chunk splitting needed?

Vite's default configuration is already good enough, but there are indeed scenarios that require manual intervention:

Scenario Custom needed? Explanation
Project just started, first-screen 800ms ❌ No Don't touch it, it's already good
First-screen load 3s+, low Lighthouse score ⚠️ Locate first Find the bottleneck before acting
A large library (e.g., ECharts) not needed on first-screen ✅ Yes Split it out for lazy loading
Multiple entry points share a lot of common code ✅ Yes Extract a common chunk
Users generally on weak networks ⚠️ Be cautious Requires finer control, but still needs data support

Step 4: If you really need to split chunks, how?

If custom chunk splitting is indeed necessary, remember a few principles:

  1. Don't stuff everything into one vendor
  2. Split by package name, not by type
  3. Don't split resources on the critical path too finely
  4. Always validate with data after changes

A relatively safe chunk splitting scheme:

ts

manualChunks(id) {
  if (id.includes('node_modules')) {
    // Split each package individually, not merge all
    const pkgName = id.split('node_modules/')[1].split('/')[0]
    return `vendor-${pkgName}`
  }
}

Or, directly use the official Vite plugin, one line:

ts

import { splitVendorChunkPlugin } from 'vite'

export default defineConfig({
  plugins: [splitVendorChunkPlugin()]
})

Final thoughts

Vite team's default configuration is much smarter than your intuitive manual configuration.

This is not to praise Vite, but a simple truth:

If a tool has been validated by thousands of projects, its default configuration is likely optimal in most scenarios. As users, before trying to "optimize" it, we should first understand:

  1. Why is the default configuration designed this way?
  2. Does my scenario really need customization?
  3. Is there data supporting my optimization direction?

This "reverse optimization" taught me one thing:

The essence of optimization is solving problems, not satisfying the psychological need to "tinker." Data first, results speak.

If you are currently looking at Vite's configuration documentation, pondering how to "optimize" the chunk splitting strategy, my advice is:

First run Lighthouse, see what the LCP actually is. If the data tells you "no problem," then really don't touch it.

After all, some optimizations are best left undone.


If this article saved you a few hours (or even an afternoon) of tinkering, give it a like before you go 👋


This article was originally published on Juejin. Feel free to share and discuss. What "reverse optimization" cases have you encountered in your projects? Share in the comments 👇