editor: lazy load

This makes the required changes needed to make the editor components
and sagas lazy-load to improve the time before the first render of the
app.

This is done by making sure we only import types from the monaco editor
module anywhere other than the saga and the component modules and using
React.lazy to lazy-load the component and start the sagas.

This cuts the main bundle time to less than 1/2 so load time should be
over twice as fast now.
This commit is contained in:
David Lechner
2022-10-27 13:17:51 -05:00
committed by David Lechner
parent 0f56bb7174
commit c49f37f5e4
9 changed files with 79 additions and 36 deletions
+3
View File
@@ -7,6 +7,9 @@
### Added
- Added feature to close editor tab on middle click ([support#758]).
### Fixed
- Fixed long time until UI visible on resource-constrained systems.
### Removed
- Removed word-based autocompletion ([support#757]).
+19 -3
View File
@@ -3,13 +3,12 @@
import 'react-splitter-layout/lib/index.css';
import './app.scss';
import { Classes } from '@blueprintjs/core';
import { Classes, Spinner } from '@blueprintjs/core';
import docsPackage from '@pybricks/ide-docs/package.json';
import React, { useEffect, useState } from 'react';
import SplitterLayout from 'react-splitter-layout';
import { useLocalStorage, useTernaryDarkMode } from 'usehooks-ts';
import Activities from '../activities/Activities';
import Editor from '../editor/Editor';
import { InstallPybricksDialog } from '../firmware/installPybricksDialog/InstallPybricksDialog';
import RestoreOfficialDialog from '../firmware/restoreOfficialDialog/RestoreOfficialDialog';
import { useSettingIsShowDocsEnabled } from '../settings/hooks';
@@ -20,6 +19,19 @@ import Tour from '../tour/Tour';
import { isMacOS } from '../utils/os';
import { httpServerHeadersVersion } from './constants';
const Editor = React.lazy(async () => {
const [sagaModule, componentModule] = await Promise.all([
import('../editor/sagas'),
import('../editor/Editor'),
]);
window.dispatchEvent(
new CustomEvent('pb-lazy-saga', { detail: { saga: sagaModule.default } }),
);
return componentModule;
});
const Docs: React.VFC = () => {
const { setIsSettingShowDocsEnabled } = useSettingIsShowDocsEnabled();
@@ -186,7 +198,11 @@ const App: React.VFC = () => {
>
<div className="pb-app-editor">
<Toolbar />
<Editor />
<React.Suspense
fallback={<Spinner className="pb-editor" />}
>
<Editor />
</React.Suspense>
</div>
<div className="pb-app-terminal">
<Terminal />
+3 -3
View File
@@ -5,7 +5,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { monaco } from 'react-monaco-editor';
import type { monaco } from 'react-monaco-editor';
/** The Pybricks MicroPython language identifier. */
export const pybricksMicroPythonId = 'pybricks-micropython';
@@ -39,7 +39,7 @@ export const conf: monaco.languages.LanguageConfiguration = {
beforeText: new RegExp(
'^\\s*(?:def|class|for|if|elif|else|while|try|with|finally|except|async).*?:\\s*$',
),
action: { indentAction: monaco.languages.IndentAction.Indent },
action: { indentAction: <monaco.languages.IndentAction.Indent>1 },
},
],
folding: {
@@ -361,7 +361,7 @@ export const templateSnippetCompletions = <monaco.languages.CompletionItemProvid
.filter((x) => x.label.startsWith(textUntilPosition))
.map<monaco.languages.CompletionItem>((x) => ({
detail: x.insertText,
kind: monaco.languages.CompletionItemKind.Snippet,
kind: <monaco.languages.CompletionItemKind.Snippet>27,
range,
...x,
}));
+28
View File
@@ -0,0 +1,28 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2022 The Pybricks Authors
import { SagaGenerator, getContext, put, select, take } from 'typed-redux-saga/macro';
import { RootState } from '../reducers';
import { editorGetValueRequest, editorGetValueResponse } from './actions';
/**
* Saga that gets the current value from the editor.
* @returns The value.
* @throws Error if editor.isReady state is false.
*/
export function* editorGetValue(): SagaGenerator<string> {
const nextMessageId = yield* getContext<() => number>('nextMessageId');
const isReady = yield* select((s: RootState) => s.editor.isReady);
if (!isReady) {
throw new Error('editorGetValue() called before editor.isReady');
}
const request = yield* put(editorGetValueRequest(nextMessageId()));
const response = yield* take(
editorGetValueResponse.when((a) => a.id === request.id),
);
return response.value;
}
-23
View File
@@ -5,7 +5,6 @@ import type { DatabaseChangeType, IDatabaseChange } from 'dexie-observable/api';
import { monaco } from 'react-monaco-editor';
import { EventChannel, buffers, eventChannel } from 'redux-saga';
import {
SagaGenerator,
call,
cancelled,
delay,
@@ -67,28 +66,6 @@ import { EditorError } from './error';
import { ActiveFileHistoryManager, OpenFileManager } from './lib';
import { pybricksMicroPythonId } from './pybricksMicroPython';
/**
* Saga that gets the current value from the editor.
* @returns The value.
* @throws Error if editor.isReady state is false.
*/
export function* editorGetValue(): SagaGenerator<string> {
const nextMessageId = yield* getContext<() => number>('nextMessageId');
const isReady = yield* select((s: RootState) => s.editor.isReady);
if (!isReady) {
throw new Error('editorGetValue() called before editor.isReady');
}
const request = yield* put(editorGetValueRequest(nextMessageId()));
const response = yield* take(
editorGetValueResponse.when((a) => a.id === request.id),
);
return response.value;
}
function* handleEditorGetValueRequest(
editor: monaco.editor.ICodeEditor,
action: ReturnType<typeof editorGetValueRequest>,
+2 -2
View File
@@ -4,7 +4,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { monaco } from 'react-monaco-editor';
import type { monaco } from 'react-monaco-editor';
export class UntitledHintContribution implements monaco.editor.IEditorContribution {
public static readonly ID = 'editor.contrib.untitledHint';
@@ -79,7 +79,7 @@ class UntitledHintContentWidget implements monaco.editor.IContentWidget {
getPosition(): monaco.editor.IContentWidgetPosition | null {
return {
position: { lineNumber: 1, column: 1 },
preference: [monaco.editor.ContentWidgetPositionPreference.EXACT],
preference: [<monaco.editor.ContentWidgetPositionPreference.EXACT>0],
};
}
+1 -1
View File
@@ -27,7 +27,7 @@ import {
} from '../ble-pybricks-service/actions';
import { FileFormat } from '../ble-pybricks-service/protocol';
import { bleDidConnectPybricks } from '../ble/actions';
import { editorGetValue } from '../editor/sagas';
import { editorGetValue } from '../editor/sagaLib';
import {
compile,
didCompile,
+1 -1
View File
@@ -6,7 +6,7 @@ import wasmV5 from '@pybricks/mpy-cross-v5/build/mpy-cross.wasm';
import { compile as mpyCrossCompileV6 } from '@pybricks/mpy-cross-v6';
import wasmV6 from '@pybricks/mpy-cross-v6/build/mpy-cross-v6.wasm';
import { call, getContext, put, select, takeEvery } from 'typed-redux-saga/macro';
import { editorGetValue } from '../editor/sagas';
import { editorGetValue } from '../editor/sagaLib';
import { FileContents, FileStorageDb } from '../fileStorage';
import { findImportedModules, resolveModule } from '../pybricksMicropython/lib';
import { RootState } from '../reducers';
+22 -3
View File
@@ -1,12 +1,12 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020-2022 The Pybricks Authors
import { all } from 'typed-redux-saga/macro';
import { eventChannel } from 'redux-saga';
import { all, spawn, take } from 'typed-redux-saga/macro';
import alerts, { AlertsSagaContext } from './alerts/sagas';
import app from './app/sagas';
import blePybricksService from './ble-pybricks-service/sagas';
import ble from './ble/sagas';
import editor from './editor/sagas';
import errorLog from './error-log/sagas';
import explorer from './explorer/sagas';
import fileStorage, { FileStorageSageContext } from './fileStorage/sagas';
@@ -18,6 +18,25 @@ import mpy from './mpy/sagas';
import notifications from './notifications/sagas';
import terminal, { TerminalSagaContext } from './terminal/sagas';
/**
* Listens to the 'pb-lazy-saga' event to spawn sagas from a React.lazy() initializer.
*/
function* lazySagas(): Generator {
const chan = eventChannel<CustomEvent<{ saga: () => void }>>((emit) => {
window.addEventListener('pb-lazy-saga', emit as EventListener);
return () => window.removeEventListener('pb-lazy-saga', emit as EventListener);
});
try {
for (;;) {
const event = yield* take(chan);
yield* spawn(event.detail.saga);
}
} finally {
chan.close();
}
}
/* istanbul ignore next */
export default function* (): Generator {
yield* all([
@@ -25,7 +44,6 @@ export default function* (): Generator {
app(),
blePybricksService(),
ble(),
editor(),
fileStorage(),
lwp3BootloaderBle(),
lwp3BootloaderProtocol(),
@@ -36,6 +54,7 @@ export default function* (): Generator {
mpy(),
notifications(),
terminal(),
lazySagas(),
]);
}