1
+ const { resolve } = require ( 'path' ) ;
2
+ const fs = require ( 'fs' ) ;
3
+
4
+ console . log ( 'Starting bundle analysis...' ) ;
5
+
6
+ // Find the most recent Next.js build manifest
7
+ const buildManifestPath = resolve ( '.next' , 'build-manifest.json' ) ;
8
+
9
+ console . log ( `Checking for build manifest at: ${ buildManifestPath } ` ) ;
10
+
11
+ if ( ! fs . existsSync ( buildManifestPath ) ) {
12
+ console . error ( 'Build manifest not found. Run `npm run build` first.' ) ;
13
+ process . exit ( 1 ) ;
14
+ }
15
+
16
+ console . log ( 'Found build manifest. Loading...' ) ;
17
+ const buildManifest = require ( buildManifestPath ) ;
18
+
19
+ // Extract all JavaScript chunks
20
+ const jsFiles = new Set ( ) ;
21
+
22
+ // Process polyfillFiles, devFiles, etc.
23
+ const manifestCategories = [
24
+ 'polyfillFiles' ,
25
+ 'devFiles' ,
26
+ 'ampDevFiles' ,
27
+ 'lowPriorityFiles' ,
28
+ 'rootMainFiles'
29
+ ] ;
30
+
31
+ manifestCategories . forEach ( category => {
32
+ if ( buildManifest [ category ] && Array . isArray ( buildManifest [ category ] ) ) {
33
+ buildManifest [ category ] . forEach ( file => {
34
+ if ( file . endsWith ( '.js' ) ) {
35
+ jsFiles . add ( file ) ;
36
+ }
37
+ } ) ;
38
+ }
39
+ } ) ;
40
+
41
+ // Process pages
42
+ if ( buildManifest . pages ) {
43
+ Object . values ( buildManifest . pages ) . forEach ( filesArray => {
44
+ if ( Array . isArray ( filesArray ) ) {
45
+ filesArray . forEach ( file => {
46
+ if ( file . endsWith ( '.js' ) ) {
47
+ jsFiles . add ( file ) ;
48
+ }
49
+ } ) ;
50
+ }
51
+ } ) ;
52
+ }
53
+
54
+ console . log ( `Found ${ jsFiles . size } JavaScript files in the build manifest.` ) ;
55
+
56
+ // Create stats for files
57
+ const fileStats = Array . from ( jsFiles ) . map ( file => {
58
+ const fullPath = resolve ( '.next' , file ) ;
59
+ console . log ( `Checking file size for: ${ fullPath } ` ) ;
60
+
61
+ let size = 0 ;
62
+ try {
63
+ const stat = fs . statSync ( fullPath ) ;
64
+ size = stat . size ;
65
+ console . log ( `File size: ${ size } bytes` ) ;
66
+ } catch ( e ) {
67
+ console . warn ( `Couldn't find file: ${ fullPath } ` , e ) ;
68
+ }
69
+
70
+ // Extract chunk name from path
71
+ const name = file . split ( '/' ) . pop ( ) ;
72
+
73
+ return {
74
+ path : file ,
75
+ name : name ,
76
+ size : size ,
77
+ sizeKB : ( size / 1024 ) . toFixed ( 2 )
78
+ } ;
79
+ } ) ;
80
+
81
+ // Sort by size, largest first
82
+ fileStats . sort ( ( a , b ) => b . size - a . size ) ;
83
+
84
+ // Group by directories
85
+ const groupedStats = { } ;
86
+ fileStats . forEach ( stat => {
87
+ const parts = stat . path . split ( '/' ) ;
88
+ const dir = parts . length > 2 ? parts . slice ( 0 , - 1 ) . join ( '/' ) : 'root' ;
89
+
90
+ if ( ! groupedStats [ dir ] ) {
91
+ groupedStats [ dir ] = [ ] ;
92
+ }
93
+
94
+ groupedStats [ dir ] . push ( stat ) ;
95
+ } ) ;
96
+
97
+ // Calculate total size
98
+ const totalSize = fileStats . reduce ( ( sum , file ) => sum + file . size , 0 ) ;
99
+ const totalSizeKB = ( totalSize / 1024 ) . toFixed ( 2 ) ;
100
+ const totalSizeMB = ( totalSize / ( 1024 * 1024 ) ) . toFixed ( 2 ) ;
101
+
102
+ // Generate HTML report
103
+ const htmlReport = `
104
+ <!DOCTYPE html>
105
+ <html lang="en">
106
+ <head>
107
+ <meta charset="UTF-8">
108
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
109
+ <title>Next.js Bundle Analysis</title>
110
+ <style>
111
+ body {
112
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
113
+ max-width: 1200px;
114
+ margin: 0 auto;
115
+ padding: 20px;
116
+ color: #333;
117
+ }
118
+ h1, h2 {
119
+ border-bottom: 1px solid #eee;
120
+ padding-bottom: 10px;
121
+ }
122
+ table {
123
+ width: 100%;
124
+ border-collapse: collapse;
125
+ margin-bottom: 30px;
126
+ }
127
+ th, td {
128
+ text-align: left;
129
+ padding: 12px;
130
+ border-bottom: 1px solid #ddd;
131
+ }
132
+ th {
133
+ background-color: #f2f2f2;
134
+ }
135
+ tr:hover {
136
+ background-color: #f5f5f5;
137
+ }
138
+ .group-header {
139
+ margin-top: 30px;
140
+ font-weight: bold;
141
+ background-color: #e6e6e6;
142
+ padding: 10px;
143
+ }
144
+ .bar {
145
+ height: 20px;
146
+ background-color: #4CAF50;
147
+ display: inline-block;
148
+ }
149
+ .summary {
150
+ background-color: #f8f9fa;
151
+ padding: 15px;
152
+ border-radius: 5px;
153
+ margin-bottom: 20px;
154
+ border-left: 5px solid #4CAF50;
155
+ }
156
+ </style>
157
+ </head>
158
+ <body>
159
+ <h1>Next.js Bundle Analysis</h1>
160
+
161
+ <div class="summary">
162
+ <h2>Summary</h2>
163
+ <p>Total JavaScript size: ${ totalSizeKB } KB (${ totalSizeMB } MB)</p>
164
+ <p>Total chunks: ${ fileStats . length } </p>
165
+ </div>
166
+
167
+ <h2>All Chunks by Size</h2>
168
+ <table>
169
+ <thead>
170
+ <tr>
171
+ <th>File</th>
172
+ <th>Size (KB)</th>
173
+ <th>Percentage</th>
174
+ </tr>
175
+ </thead>
176
+ <tbody>
177
+ ${ fileStats . map ( file => {
178
+ const percentage = ( ( file . size / totalSize ) * 100 ) . toFixed ( 2 ) ;
179
+ const barWidth = Math . max ( 1 , percentage ) ;
180
+ return `
181
+ <tr>
182
+ <td>${ file . path } </td>
183
+ <td>${ file . sizeKB } KB</td>
184
+ <td>
185
+ <div class="bar" style="width: ${ barWidth } %"></div>
186
+ ${ percentage } %
187
+ </td>
188
+ </tr>
189
+ ` ;
190
+ } ) . join ( '' ) }
191
+ </tbody>
192
+ </table>
193
+
194
+ <h2>Chunks by Directory</h2>
195
+ ${ Object . entries ( groupedStats ) . map ( ( [ dir , files ] ) => {
196
+ const dirSize = files . reduce ( ( sum , file ) => sum + file . size , 0 ) ;
197
+ const dirSizeKB = ( dirSize / 1024 ) . toFixed ( 2 ) ;
198
+ const dirPercentage = ( ( dirSize / totalSize ) * 100 ) . toFixed ( 2 ) ;
199
+
200
+ return `
201
+ <div class="group-header">${ dir } (${ dirSizeKB } KB, ${ dirPercentage } % of total)</div>
202
+ <table>
203
+ <thead>
204
+ <tr>
205
+ <th>File</th>
206
+ <th>Size (KB)</th>
207
+ <th>Percentage</th>
208
+ </tr>
209
+ </thead>
210
+ <tbody>
211
+ ${ files . map ( file => {
212
+ const percentage = ( ( file . size / dirSize ) * 100 ) . toFixed ( 2 ) ;
213
+ const barWidth = Math . max ( 1 , percentage ) ;
214
+ return `
215
+ <tr>
216
+ <td>${ file . name } </td>
217
+ <td>${ file . sizeKB } KB</td>
218
+ <td>
219
+ <div class="bar" style="width: ${ barWidth } %"></div>
220
+ ${ percentage } %
221
+ </td>
222
+ </tr>
223
+ ` ;
224
+ } ) . join ( '' ) }
225
+ </tbody>
226
+ </table>
227
+ ` ;
228
+ } ) . join ( '' ) }
229
+ </body>
230
+ </html>
231
+ ` ;
232
+
233
+ // Write HTML report
234
+ const reportPath = 'bundle-report.html' ;
235
+ fs . writeFileSync ( reportPath , htmlReport ) ;
236
+
237
+ console . log ( `Bundle analysis report generated: ${ reportPath } ` ) ;
238
+ console . log ( 'Open this file in your browser to view the report.' )
0 commit comments