Skip to content

Commit 2f704ba

Browse files
⚡ [#76] Create separate chunk for Formio/lodash
We can't properly extract lodash into a separate chunk because *many* modules in formio import lodash as a whole rather than just the modules they need, breaking tree shaking. It also turns out (via 'npx vite-bundle-visualizer') that formio itself is quite chonky and for the initial SDK load/routing we don't need Formio yet, so we can defer loading that with a dynamic import. This dynamic import ensures the formio-init code ends up in its own chunk.
1 parent 49bd7cb commit 2f704ba

File tree

3 files changed

+125
-89
lines changed

3 files changed

+125
-89
lines changed

src/components/FormStep/index.jsx

Lines changed: 71 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,7 @@
2424
import cloneDeep from 'lodash/cloneDeep';
2525
import isEmpty from 'lodash/isEmpty';
2626
import isEqual from 'lodash/isEqual';
27-
import {useContext, useRef} from 'react';
28-
import {Form} from 'react-formio';
27+
import {Suspense, lazy, useContext, useRef} from 'react';
2928
import {useIntl} from 'react-intl';
3029
import {useNavigate, useParams} from 'react-router-dom';
3130
import {useAsync} from 'react-use';
@@ -55,6 +54,17 @@ import useTitle from 'hooks/useTitle';
5554

5655
import {doLogicCheck, getCustomValidationHook, submitStepData} from './data';
5756

57+
// Dynamically import react-formio and use React.lazy to facilitate bundle splitting
58+
// into separate chunks.
59+
const Form = lazy(async () => {
60+
// this should already have been resolved the the sdk.jsx entrypoint :)
61+
const {Form, initializeFormio} = await import('formio-init');
62+
// side effect to ensure our custom templates/module are set up
63+
initializeFormio();
64+
// React.lazy must yield a 'default export'
65+
return {default: Form};
66+
});
67+
5868
/**
5969
* Debounce interval in milliseconds (1000ms equals 1s) to prevent excessive amount of logic checks.
6070
* @type {number}
@@ -768,64 +778,66 @@ const FormStep = () => {
768778
headingType="subtitle"
769779
modifiers={['padded']}
770780
/>
771-
<form onSubmit={onReactSubmit} noValidate>
772-
<Form
773-
ref={formRef}
774-
form={configuration}
775-
onChange={onFormIOChange}
776-
onSubmit={onFormIOSubmit}
777-
onInitialized={onFormIOInitialized}
778-
options={{
779-
noAlerts: true,
780-
baseUrl: config.baseUrl,
781-
language: formioTranslations.language,
782-
i18n: formioTranslations.i18n,
783-
evalContext: {
784-
ofPrefix: `${PREFIX}-`,
785-
requiredFieldsWithAsterisk: config.requiredFieldsWithAsterisk,
786-
},
787-
hooks: {
788-
customValidation: getCustomValidationHook(submissionStep.url, error =>
789-
dispatch({type: 'ERROR', payload: error})
790-
),
791-
},
792-
// custom options
793-
intl,
794-
ofContext: {
795-
form: form,
796-
submissionUuid: submission.id,
797-
submissionUrl: submission.url,
798-
saveStepData: async () =>
799-
await submitStepData(submissionStep.url, {...getCurrentFormData()}),
800-
verifyEmailCallback: ({key, email}) => {
801-
// clear the errors from the component
802-
const formInstance = formRef.current.formio;
803-
const component = formInstance.getComponent(key);
804-
component.setCustomValidity('');
805-
806-
dispatch({
807-
type: 'VERIFY_EMAIL',
808-
payload: {componentKey: key, emailAddress: email},
809-
});
781+
<Suspense fallback={<Loader modifiers={['centered']} />}>
782+
<form onSubmit={onReactSubmit} noValidate>
783+
<Form
784+
ref={formRef}
785+
form={configuration}
786+
onChange={onFormIOChange}
787+
onSubmit={onFormIOSubmit}
788+
onInitialized={onFormIOInitialized}
789+
options={{
790+
noAlerts: true,
791+
baseUrl: config.baseUrl,
792+
language: formioTranslations.language,
793+
i18n: formioTranslations.i18n,
794+
evalContext: {
795+
ofPrefix: `${PREFIX}-`,
796+
requiredFieldsWithAsterisk: config.requiredFieldsWithAsterisk,
797+
},
798+
hooks: {
799+
customValidation: getCustomValidationHook(submissionStep.url, error =>
800+
dispatch({type: 'ERROR', payload: error})
801+
),
802+
},
803+
// custom options
804+
intl,
805+
ofContext: {
806+
form: form,
807+
submissionUuid: submission.id,
808+
submissionUrl: submission.url,
809+
saveStepData: async () =>
810+
await submitStepData(submissionStep.url, {...getCurrentFormData()}),
811+
verifyEmailCallback: ({key, email}) => {
812+
// clear the errors from the component
813+
const formInstance = formRef.current.formio;
814+
const component = formInstance.getComponent(key);
815+
component.setCustomValidity('');
816+
817+
dispatch({
818+
type: 'VERIFY_EMAIL',
819+
payload: {componentKey: key, emailAddress: email},
820+
});
821+
},
810822
},
811-
},
812-
}}
813-
/>
814-
{config.debug ? <FormStepDebug data={getCurrentFormData()} /> : null}
815-
<ButtonsToolbar
816-
canSubmitStep={canSubmit}
817-
canSubmitForm={submission.submissionAllowed}
818-
canSuspendForm={form.suspensionAllowed}
819-
isAuthenticated={submission.isAuthenticated}
820-
isLastStep={isLastStep(currentStepIndex, submission)}
821-
isCheckingLogic={logicChecking}
822-
loginRequired={form.loginRequired}
823-
onFormSave={onFormSave}
824-
onNavigatePrevPage={onPrevPage}
825-
previousPage={previousPage}
826-
onDestroySession={onDestroySession}
827-
/>
828-
</form>
823+
}}
824+
/>
825+
{config.debug ? <FormStepDebug data={getCurrentFormData()} /> : null}
826+
<ButtonsToolbar
827+
canSubmitStep={canSubmit}
828+
canSubmitForm={submission.submissionAllowed}
829+
canSuspendForm={form.suspensionAllowed}
830+
isAuthenticated={submission.isAuthenticated}
831+
isLastStep={isLastStep(currentStepIndex, submission)}
832+
isCheckingLogic={logicChecking}
833+
loginRequired={form.loginRequired}
834+
onFormSave={onFormSave}
835+
onNavigatePrevPage={onPrevPage}
836+
previousPage={previousPage}
837+
onDestroySession={onDestroySession}
838+
/>
839+
</form>
840+
</Suspense>
829841
</>
830842
) : null}
831843
</Card>

src/formio-init.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/**
2+
* This module is intended to be lazy loaded so that Vite can split the bundle in
3+
* separate chunks.
4+
*/
5+
import ProtectedEval from '@formio/protected-eval';
6+
import lodash from 'lodash';
7+
import {Form, Formio, Templates} from 'react-formio';
8+
9+
import OpenFormsModule from './formio/module';
10+
import {AddFetchAuth} from './formio/plugins';
11+
import OFLibrary from './formio/templates';
12+
13+
let initialized = false;
14+
15+
/**
16+
* Initialize our Formio customization.
17+
*/
18+
export const initializeFormio = () => {
19+
if (initialized) return;
20+
21+
// lodash must be bundled for Formio templates to work properly...
22+
if (typeof window !== 'undefined') {
23+
window._ = lodash;
24+
}
25+
26+
// use protected eval to not rely on unsafe-eval (CSP)
27+
// eslint-disable-next-line react-hooks/rules-of-hooks
28+
Formio.use(ProtectedEval);
29+
30+
// use custom component overrides
31+
// eslint-disable-next-line react-hooks/rules-of-hooks
32+
Formio.use(OpenFormsModule);
33+
34+
// use our own template library
35+
Templates.current = OFLibrary;
36+
37+
Formio.registerPlugin(AddFetchAuth, 'addFetchAuth');
38+
39+
Formio.libraries = {
40+
// The flatpickr css is added as part of our scss build so add empty attribute to
41+
// prevent Formio trying to get this css from a CDN
42+
'flatpickr-css': '',
43+
};
44+
45+
initialized = true;
46+
};
47+
48+
export {Form};

src/sdk.jsx

Lines changed: 6 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,28 @@
1-
import ProtectedEval from '@formio/protected-eval';
21
import 'flatpickr';
3-
import lodash from 'lodash';
42
import {fixIconUrls as fixLeafletIconUrls} from 'map';
53
import React from 'react';
64
import {createRoot} from 'react-dom/client';
7-
import {Formio, Templates} from 'react-formio';
85
import ReactModal from 'react-modal';
96
import {RouterProvider, createBrowserRouter, createHashRouter, resolvePath} from 'react-router-dom';
107
import {NonceProvider} from 'react-select';
118

129
import {ConfigContext, FormContext} from 'Context';
1310
import {get} from 'api';
1411
import {getRedirectParams} from 'components/routingActions';
15-
import {AddFetchAuth} from 'formio/plugins';
1612
import {CSPNonce} from 'headers';
1713
import {I18NErrorBoundary, I18NManager} from 'i18n';
1814
import routes from 'routes';
1915
import initialiseSentry from 'sentry';
2016
import {DEBUG, getVersion} from 'utils';
2117

22-
import OpenFormsModule from './formio/module';
23-
import OFLibrary from './formio/templates';
2418
import './styles.scss';
2519

26-
// lodash must be bundled for Formio templates to work properly...
27-
if (typeof window !== 'undefined') {
28-
window._ = lodash;
29-
}
30-
31-
// use protected eval to not rely on unsafe-eval (CSP)
32-
// eslint-disable-next-line react-hooks/rules-of-hooks
33-
Formio.use(ProtectedEval);
34-
35-
// use custom component overrides
36-
// eslint-disable-next-line react-hooks/rules-of-hooks
37-
Formio.use(OpenFormsModule);
38-
// use our own template library
39-
Templates.current = OFLibrary;
40-
41-
Formio.registerPlugin(AddFetchAuth, 'addFetchAuth');
42-
43-
Formio.libraries = {
44-
// The flatpickr css is added as part of our scss build so add empty attribute to
45-
// prevent Formio trying to get this css from a CDN
46-
'flatpickr-css': '',
47-
};
48-
4920
fixLeafletIconUrls();
5021

22+
// asynchronously 'pre-load' our formio initialization so that this can be split off
23+
// from the mainbundle into a separate chunk.
24+
import('./formio-init');
25+
5126
const VERSION = getVersion();
5227

5328
class OpenForm {
@@ -161,6 +136,7 @@ class OpenForm {
161136
this.targetNode.textContent = `Loading form...`;
162137
this.baseTitle = document.title;
163138
this.formObject = await get(this.url);
139+
164140
this.root = createRoot(this.targetNode);
165141
this.render();
166142
}
@@ -212,4 +188,4 @@ class OpenForm {
212188

213189
export default OpenForm;
214190
export {ANALYTICS_PROVIDERS} from 'hooks/usePageViews';
215-
export {VERSION, OpenForm, Formio, Templates, OFLibrary, OpenFormsModule};
191+
export {VERSION, OpenForm};

0 commit comments

Comments
 (0)