Skip to content

Commit eba166e

Browse files
claudertibbles
authored andcommitted
Implement webpack bundle size optimizations
This commit implements three high-priority fixes identified in the webpack bundle analysis that will reduce total bundle size by an estimated 2.75-4.75 MB (13-23% reduction). - File: packages/kolibri-build/src/webpack.config.base.js - Change: mangle: false → mangle: { safari10: true } - Expected savings: 2-4 MB across all bundles Terser mangling was disabled, preventing variable name shortening which typically provides 30-40% size reduction. This single-line change enables it with Safari 10 compatibility. - File: packages/kolibri/composables/useUser.js - Change: import { pick } from 'lodash' → import pick from 'lodash/pick' - Expected savings: ~450 KB in core bundle A single bad import was causing webpack to bundle the entire lodash library (459 KB) instead of just the pick function (~5 KB). - Files: packages/kolibri-i18n/src/intl_code_gen.js (generator) packages/kolibri/utils/i18n.js (consumer) packages/kolibri/utils/internal/vue-intl-locale-data.js (generated) - Expected savings: ~300 KB Root cause: The intl polyfill locale data was correctly lazy-loaded, but vue-intl locale data was not. The code generator was producing synchronous require() for all 30+ vue-intl locales, bundling them in the main chunk (336 KB total). Fix: Updated code generator to use require.ensure() pattern (same as intl), and updated consumption code to handle async loading. Now only the current language's vue-intl locale data is loaded.
1 parent d26f50b commit eba166e

File tree

5 files changed

+249
-51
lines changed

5 files changed

+249
-51
lines changed

packages/kolibri-build/src/webpack.config.base.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,9 @@ module.exports = ({ mode = 'development', hot = false, cache = false, transpile
110110
new TerserPlugin({
111111
parallel: true,
112112
terserOptions: {
113-
mangle: false,
113+
mangle: {
114+
safari10: true,
115+
},
114116
safari10: true,
115117
output: {
116118
comments: false,

packages/kolibri-i18n/src/intl_code_gen.js

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ const path = require('path');
22
const fs = require('fs');
33
const { writeSourceToFile } = require('kolibri-format');
44
const logger = require('kolibri-logging');
5+
const uniqBy = require('lodash/uniqBy');
56

67
const logging = logger.getLogger('Kolibri Intl Data');
78

@@ -49,13 +50,22 @@ module.exports = function (outputDir, languageInfoPath) {
4950
* Polyfill files are copied to ./polyfills/ directory to avoid external dependencies.
5051
*/
5152
`;
52-
const vueIntlHeader = `module.exports = function () {
53-
const data = [];`;
53+
const vueIntlHeader = `module.exports = function (locale) {
54+
switch (locale) {`;
5455

5556
function generateVueIntlItems(language) {
5657
/*
57-
* Generate entries of this form:
58-
* data.push(require('vue-intl/locale-data/ar.js'));
58+
* Generate entries of this form with lazy loading:
59+
*
60+
* case 'ar':
61+
* return new Promise(function(resolve) {
62+
* require.ensure(
63+
* ['vue-intl/locale-data/ar.js'],
64+
* function(require) {
65+
* resolve(() => require('vue-intl/locale-data/ar.js'));
66+
* }
67+
* );
68+
* });
5969
*
6070
* Some Intl codes look like 'ar' and others look like 'bn-bd', so for Vue Intl
6171
* we strip off the territory code if it's there.
@@ -88,17 +98,40 @@ module.exports = function (outputDir, languageInfoPath) {
8898
}
8999
}
90100

91-
return `data.push(require('${module_path}'));`;
101+
return `
102+
case '${vue_intl_code}':
103+
return new Promise(function(resolve) {
104+
require.ensure(
105+
['${module_path}'],
106+
function(require) {
107+
resolve(() => require('${module_path}'));
108+
}
109+
);
110+
});`;
92111
}
93112
}
94113

95114
const vueIntlFooter = `
96-
return data;
115+
default:
116+
return new Promise(function(resolve) {
117+
require.ensure(
118+
['vue-intl/locale-data/en.js'],
119+
function(require) {
120+
resolve(() => require('vue-intl/locale-data/en.js'));
121+
}
122+
);
123+
});
124+
}
97125
};
98126
`;
99127

100128
const vueIntlModule =
101-
commonHeader + vueIntlHeader + languageInfo.map(generateVueIntlItems).join('') + vueIntlFooter;
129+
commonHeader +
130+
vueIntlHeader +
131+
uniqBy(languageInfo, l => l.intl_code.split('-')[0])
132+
.map(generateVueIntlItems)
133+
.join('') +
134+
vueIntlFooter;
102135

103136
const vueIntlModulePath = path.resolve(outputDir, 'vue-intl-locale-data.js');
104137
const intlHeader = `module.exports = function(locale) {

packages/kolibri/composables/useUser.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import Lockr from 'lockr';
88
import urls from 'kolibri/urls';
99
import store from 'kolibri/store';
1010
import { LoginErrors, ERROR_CONSTANTS, UPDATE_MODAL_DISMISSED, UserKinds } from 'kolibri/constants';
11-
import { pick } from 'lodash';
11+
import pick from 'lodash/pick';
1212

1313
// Base session state (migrated from session module)
1414
const baseSessionState = {

packages/kolibri/utils/i18n.js

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -228,9 +228,15 @@ function _setUpVueIntl() {
228228
if (languageGlobals.coreLanguageMessages) {
229229
Vue.registerMessages(currentLanguage, languageGlobals.coreLanguageMessages);
230230
}
231-
importVueIntlLocaleData().forEach(localeData => VueIntl.addLocaleData(localeData));
232231

233-
_i18nReady = true;
232+
// Load vue-intl locale data asynchronously for current language
233+
// Extract just the language code (e.g., 'en' from 'en-us') for vue-intl
234+
const vueIntlLanguageCode = currentLanguage.split('-')[0];
235+
return importVueIntlLocaleData(vueIntlLanguageCode).then(requireLocaleData => {
236+
const localeData = requireLocaleData();
237+
VueIntl.addLocaleData(localeData);
238+
_i18nReady = true;
239+
});
234240
}
235241

236242
export function i18nSetup(skipPolyfill = false) {
@@ -246,8 +252,7 @@ export function i18nSetup(skipPolyfill = false) {
246252
// the modules need to wait until that happens.
247253
return new Promise((resolve, reject) => {
248254
if (Object.prototype.hasOwnProperty.call(global, 'Intl') || skipPolyfill) {
249-
_setUpVueIntl();
250-
resolve();
255+
_setUpVueIntl().then(resolve, reject);
251256
} else {
252257
Promise.all([
253258
new Promise(res => {
@@ -265,8 +270,7 @@ export function i18nSetup(skipPolyfill = false) {
265270
([requireIntl, requireIntlLocaleData]) => {
266271
requireIntl(); // requireIntl must run before requireIntlLocaleData
267272
requireIntlLocaleData();
268-
_setUpVueIntl();
269-
resolve();
273+
_setUpVueIntl().then(resolve, reject);
270274
},
271275
error => {
272276
logging.error(error);

packages/kolibri/utils/internal/vue-intl-locale-data.js

Lines changed: 195 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -8,40 +8,199 @@
88
*
99
* Polyfill files are copied to ./polyfills/ directory to avoid external dependencies.
1010
*/
11-
module.exports = function () {
12-
const data = [];
13-
data.push(require('vue-intl/locale-data/ar.js'));
14-
data.push(require('vue-intl/locale-data/bg.js'));
15-
data.push(require('vue-intl/locale-data/bn.js'));
16-
data.push(require('vue-intl/locale-data/de.js'));
17-
data.push(require('vue-intl/locale-data/el.js'));
18-
data.push(require('vue-intl/locale-data/en.js'));
19-
data.push(require('vue-intl/locale-data/es.js'));
20-
data.push(require('vue-intl/locale-data/es.js'));
21-
data.push(require('vue-intl/locale-data/fa.js'));
22-
data.push(require('vue-intl/locale-data/fr.js'));
23-
data.push(require('vue-intl/locale-data/ff.js'));
24-
data.push(require('vue-intl/locale-data/gu.js'));
25-
data.push(require('vue-intl/locale-data/ha.js'));
26-
data.push(require('vue-intl/locale-data/hi.js'));
27-
data.push(require('./polyfills/vue-intl/ht.js'));
28-
data.push(require('vue-intl/locale-data/id.js'));
29-
data.push(require('vue-intl/locale-data/it.js'));
30-
data.push(require('vue-intl/locale-data/ka.js'));
31-
data.push(require('vue-intl/locale-data/km.js'));
32-
data.push(require('vue-intl/locale-data/ko.js'));
33-
data.push(require('vue-intl/locale-data/mr.js'));
34-
data.push(require('vue-intl/locale-data/my.js'));
35-
data.push(require('vue-intl/locale-data/ny.js'));
36-
data.push(require('vue-intl/locale-data/pa.js'));
37-
data.push(require('vue-intl/locale-data/pt.js'));
38-
data.push(require('vue-intl/locale-data/pt.js'));
39-
data.push(require('vue-intl/locale-data/sw.js'));
40-
data.push(require('vue-intl/locale-data/te.js'));
41-
data.push(require('vue-intl/locale-data/uk.js'));
42-
data.push(require('vue-intl/locale-data/ur.js'));
43-
data.push(require('vue-intl/locale-data/vi.js'));
44-
data.push(require('vue-intl/locale-data/yo.js'));
45-
data.push(require('vue-intl/locale-data/zh.js'));
46-
return data;
11+
module.exports = function (locale) {
12+
switch (locale) {
13+
case 'ar':
14+
return new Promise(function (resolve) {
15+
require.ensure(['vue-intl/locale-data/ar.js'], function (require) {
16+
resolve(() => require('vue-intl/locale-data/ar.js'));
17+
});
18+
});
19+
case 'bg':
20+
return new Promise(function (resolve) {
21+
require.ensure(['vue-intl/locale-data/bg.js'], function (require) {
22+
resolve(() => require('vue-intl/locale-data/bg.js'));
23+
});
24+
});
25+
case 'bn':
26+
return new Promise(function (resolve) {
27+
require.ensure(['vue-intl/locale-data/bn.js'], function (require) {
28+
resolve(() => require('vue-intl/locale-data/bn.js'));
29+
});
30+
});
31+
case 'de':
32+
return new Promise(function (resolve) {
33+
require.ensure(['vue-intl/locale-data/de.js'], function (require) {
34+
resolve(() => require('vue-intl/locale-data/de.js'));
35+
});
36+
});
37+
case 'el':
38+
return new Promise(function (resolve) {
39+
require.ensure(['vue-intl/locale-data/el.js'], function (require) {
40+
resolve(() => require('vue-intl/locale-data/el.js'));
41+
});
42+
});
43+
case 'en':
44+
return new Promise(function (resolve) {
45+
require.ensure(['vue-intl/locale-data/en.js'], function (require) {
46+
resolve(() => require('vue-intl/locale-data/en.js'));
47+
});
48+
});
49+
case 'es':
50+
return new Promise(function (resolve) {
51+
require.ensure(['vue-intl/locale-data/es.js'], function (require) {
52+
resolve(() => require('vue-intl/locale-data/es.js'));
53+
});
54+
});
55+
case 'fa':
56+
return new Promise(function (resolve) {
57+
require.ensure(['vue-intl/locale-data/fa.js'], function (require) {
58+
resolve(() => require('vue-intl/locale-data/fa.js'));
59+
});
60+
});
61+
case 'fr':
62+
return new Promise(function (resolve) {
63+
require.ensure(['vue-intl/locale-data/fr.js'], function (require) {
64+
resolve(() => require('vue-intl/locale-data/fr.js'));
65+
});
66+
});
67+
case 'ff':
68+
return new Promise(function (resolve) {
69+
require.ensure(['vue-intl/locale-data/ff.js'], function (require) {
70+
resolve(() => require('vue-intl/locale-data/ff.js'));
71+
});
72+
});
73+
case 'gu':
74+
return new Promise(function (resolve) {
75+
require.ensure(['vue-intl/locale-data/gu.js'], function (require) {
76+
resolve(() => require('vue-intl/locale-data/gu.js'));
77+
});
78+
});
79+
case 'ha':
80+
return new Promise(function (resolve) {
81+
require.ensure(['vue-intl/locale-data/ha.js'], function (require) {
82+
resolve(() => require('vue-intl/locale-data/ha.js'));
83+
});
84+
});
85+
case 'hi':
86+
return new Promise(function (resolve) {
87+
require.ensure(['vue-intl/locale-data/hi.js'], function (require) {
88+
resolve(() => require('vue-intl/locale-data/hi.js'));
89+
});
90+
});
91+
case 'ht':
92+
return new Promise(function (resolve) {
93+
require.ensure(['./polyfills/vue-intl/ht.js'], function (require) {
94+
resolve(() => require('./polyfills/vue-intl/ht.js'));
95+
});
96+
});
97+
case 'id':
98+
return new Promise(function (resolve) {
99+
require.ensure(['vue-intl/locale-data/id.js'], function (require) {
100+
resolve(() => require('vue-intl/locale-data/id.js'));
101+
});
102+
});
103+
case 'it':
104+
return new Promise(function (resolve) {
105+
require.ensure(['vue-intl/locale-data/it.js'], function (require) {
106+
resolve(() => require('vue-intl/locale-data/it.js'));
107+
});
108+
});
109+
case 'ka':
110+
return new Promise(function (resolve) {
111+
require.ensure(['vue-intl/locale-data/ka.js'], function (require) {
112+
resolve(() => require('vue-intl/locale-data/ka.js'));
113+
});
114+
});
115+
case 'km':
116+
return new Promise(function (resolve) {
117+
require.ensure(['vue-intl/locale-data/km.js'], function (require) {
118+
resolve(() => require('vue-intl/locale-data/km.js'));
119+
});
120+
});
121+
case 'ko':
122+
return new Promise(function (resolve) {
123+
require.ensure(['vue-intl/locale-data/ko.js'], function (require) {
124+
resolve(() => require('vue-intl/locale-data/ko.js'));
125+
});
126+
});
127+
case 'mr':
128+
return new Promise(function (resolve) {
129+
require.ensure(['vue-intl/locale-data/mr.js'], function (require) {
130+
resolve(() => require('vue-intl/locale-data/mr.js'));
131+
});
132+
});
133+
case 'my':
134+
return new Promise(function (resolve) {
135+
require.ensure(['vue-intl/locale-data/my.js'], function (require) {
136+
resolve(() => require('vue-intl/locale-data/my.js'));
137+
});
138+
});
139+
case 'ny':
140+
return new Promise(function (resolve) {
141+
require.ensure(['vue-intl/locale-data/ny.js'], function (require) {
142+
resolve(() => require('vue-intl/locale-data/ny.js'));
143+
});
144+
});
145+
case 'pa':
146+
return new Promise(function (resolve) {
147+
require.ensure(['vue-intl/locale-data/pa.js'], function (require) {
148+
resolve(() => require('vue-intl/locale-data/pa.js'));
149+
});
150+
});
151+
case 'pt':
152+
return new Promise(function (resolve) {
153+
require.ensure(['vue-intl/locale-data/pt.js'], function (require) {
154+
resolve(() => require('vue-intl/locale-data/pt.js'));
155+
});
156+
});
157+
case 'sw':
158+
return new Promise(function (resolve) {
159+
require.ensure(['vue-intl/locale-data/sw.js'], function (require) {
160+
resolve(() => require('vue-intl/locale-data/sw.js'));
161+
});
162+
});
163+
case 'te':
164+
return new Promise(function (resolve) {
165+
require.ensure(['vue-intl/locale-data/te.js'], function (require) {
166+
resolve(() => require('vue-intl/locale-data/te.js'));
167+
});
168+
});
169+
case 'uk':
170+
return new Promise(function (resolve) {
171+
require.ensure(['vue-intl/locale-data/uk.js'], function (require) {
172+
resolve(() => require('vue-intl/locale-data/uk.js'));
173+
});
174+
});
175+
case 'ur':
176+
return new Promise(function (resolve) {
177+
require.ensure(['vue-intl/locale-data/ur.js'], function (require) {
178+
resolve(() => require('vue-intl/locale-data/ur.js'));
179+
});
180+
});
181+
case 'vi':
182+
return new Promise(function (resolve) {
183+
require.ensure(['vue-intl/locale-data/vi.js'], function (require) {
184+
resolve(() => require('vue-intl/locale-data/vi.js'));
185+
});
186+
});
187+
case 'yo':
188+
return new Promise(function (resolve) {
189+
require.ensure(['vue-intl/locale-data/yo.js'], function (require) {
190+
resolve(() => require('vue-intl/locale-data/yo.js'));
191+
});
192+
});
193+
case 'zh':
194+
return new Promise(function (resolve) {
195+
require.ensure(['vue-intl/locale-data/zh.js'], function (require) {
196+
resolve(() => require('vue-intl/locale-data/zh.js'));
197+
});
198+
});
199+
default:
200+
return new Promise(function (resolve) {
201+
require.ensure(['vue-intl/locale-data/en.js'], function (require) {
202+
resolve(() => require('vue-intl/locale-data/en.js'));
203+
});
204+
});
205+
}
47206
};

0 commit comments

Comments
 (0)