Skip to content

Speed up page loads by over 10x, reduce JavaScript bundle transfers by over 18x #7889

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 36 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
5b391a8
fix(locales): invalid interpolation patterns
purfectliterature Apr 18, 2025
f48d536
fix(AchievementReordering): clarify button label
purfectliterature Apr 18, 2025
9770410
perf(AchievementReordering): lazy load jQuery, jQuery UI
purfectliterature Apr 18, 2025
f5d1f8f
fix(moment): import moment from lib/moment only
purfectliterature Apr 18, 2025
a9f62f1
perf(EditorField): lazy load Ace's language modes
purfectliterature Apr 18, 2025
35c1837
perf(VideoPlayer): import only YouTube adapter for react-player
purfectliterature Apr 18, 2025
de58b2f
perf(AuthenticatedApp): lazy load all routes
purfectliterature Apr 18, 2025
4730f44
perf(router): lazy load all routes
purfectliterature Apr 18, 2025
733681a
perf(UnauthenticatedApp): lazy load all routes
purfectliterature Apr 18, 2025
d325e82
perf(SubmissionEditIndex): lazy load all question type answer components
purfectliterature Apr 18, 2025
c2eb7d5
perf(index): remove initializers, webfontloader -> CSS `@import`
purfectliterature Apr 18, 2025
76778ad
fix(store): suppress legacy `onSubmit`, `onConfirm` console errors
purfectliterature Apr 18, 2025
09ff8c8
perf: replace lodash with lodash-es absolute imports
purfectliterature Apr 18, 2025
c57ed14
feat(webpack): allow configuring client port
purfectliterature Apr 18, 2025
3acdce4
chore(webpack): add webpack config types
purfectliterature Apr 18, 2025
19d0ade
perf(webpack): remove unused entry points
purfectliterature Apr 18, 2025
9b3a80c
perf(webpack): remove unused expose-loader
purfectliterature Apr 18, 2025
619ae49
perf(webpack): remove unused webpack-manifest-plugin
purfectliterature Apr 18, 2025
35f2230
perf(webpack): improve CSS, Sass/SCSS compilations
purfectliterature Apr 18, 2025
8ed1e4b
fix(webpack): favicon-webpack-plugin warnings in development mode
purfectliterature Apr 18, 2025
2361606
fix(webpack): ENAMETOOLONG error due to too many chunks
purfectliterature Apr 18, 2025
d5e5484
perf: partially remove PropTypes in production
purfectliterature Apr 18, 2025
678dee1
feat(webpack): always clean build folder before building
purfectliterature Apr 18, 2025
4db8dbe
perf(webpack): enable caching for production builds
purfectliterature Apr 18, 2025
05d54a4
perf(webpack): use SWC for faster & more aggressive minification
purfectliterature Apr 18, 2025
3d74ef2
perf(webpack): trim moment-timezone data to only from 2014
purfectliterature Apr 18, 2025
8a5bb3a
perf(webpack): optimise images, SVGs
purfectliterature Apr 18, 2025
17840d1
perf(webpack): generate gzip, brotli compressed bundles
purfectliterature Apr 18, 2025
66afa77
perf(i18n): lazy load translations
purfectliterature Apr 18, 2025
8f40e05
perf(CKEditorRichText): optimise build, upgrade to v45
purfectliterature Apr 18, 2025
da70363
test(client): await lazy loaded pages
purfectliterature Apr 19, 2025
937bb04
test(jest): add lodash overrides since lodash-es is ESM only
purfectliterature Apr 19, 2025
2fca6fa
test(capybara): remove jQuery check from `wait_for_ajax`
purfectliterature Apr 19, 2025
badc528
test(forum_disbursement_spec): wrong tab label
purfectliterature Apr 19, 2025
75435be
test(assessment_management_spec): fix flaky test
purfectliterature Apr 19, 2025
7941198
feat(application_html_formatters_helper): whitelist only YouTube URLs
purfectliterature Apr 28, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,5 @@ playwright/.cache/
coverage/

dump.rdb

compiled-locales
8 changes: 1 addition & 7 deletions app/helpers/application_html_formatters_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,7 @@ def self.build_html_pipeline(custom_options)
# List of video hosting site URLs to allow
VIDEO_URL_WHITELIST = Regexp.union(
/\A(?:https?:)?\/\/(?:www\.)?(?:m.)?youtube\.com\//,
/\A(?:https?:)?\/\/(?:www\.)?youtu.be\//,
/\A(?:https?:)?\/\/(?:www\.)?(?:player.)?vimeo\.com\//,
/\A(?:https?:)?\/\/(?:www\.)?vine\.co\//,
/\A(?:https?:)?\/\/(?:www\.)?instagram\.com\//,
/\A(?:https?:)?\/\/(?:www\.)?(?:geo.)?dailymotion\.com\//,
/\A(?:https?:)?\/\/(?:www\.)?dai\.ly\//,
/\A(?:https?:)?\/\/(?:www\.)?youku\.com\//
/\A(?:https?:)?\/\/(?:www\.)?youtu.be\//
).freeze

OEMBED_WHITELIST_TRANSFORMER = lambda do |env|
Expand Down
17 changes: 8 additions & 9 deletions client/.babelrc
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,6 @@
"ast": true
}
],
[
"babel-plugin-import",
{
"libraryName": "lodash",
"libraryDirectory": "",
"camel2DashComponentName": false
}
],
[
"babel-plugin-import",
{
Expand All @@ -44,7 +36,14 @@
"env": {
"production": {
"plugins": [
["react-remove-properties", { "properties": ["data-testid"] }]
["react-remove-properties", { "properties": ["data-testid"] }],
[
"transform-react-remove-prop-types",
{
"mode": "remove",
"removeImport": true
}
]
]
},
"test": {
Expand Down
6 changes: 0 additions & 6 deletions client/app/__test__/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,6 @@ import './mocks/matchMedia';

Enzyme.configure({ adapter: new Adapter() });

require('@babel/polyfill');
// Our jquery is from CDN and loaded at runtime, so this is required in test.
const jQuery = require('jquery');

const timeZone = 'Asia/Singapore';
const intlCache = createIntlCache();
const intl = createIntl({ locale: 'en', timeZone }, intlCache);
Expand Down Expand Up @@ -47,8 +43,6 @@ const buildContextOptions = (store) => {
global.courseId = courseId;
global.window = window;
global.muiTheme = muiTheme;
global.$ = jQuery;
global.jQuery = jQuery;
global.buildContextOptions = buildContextOptions;

window.history.pushState({}, '', `/courses/${courseId}`);
Expand Down
2 changes: 1 addition & 1 deletion client/app/bundles/common/DashboardPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { defineMessages } from 'react-intl';
import { Navigate } from 'react-router-dom';
import { ArrowForward } from '@mui/icons-material';
import { Avatar, Stack, Typography } from '@mui/material';
import moment from 'moment';
import { HomeLayoutCourseData } from 'types/home';

import { getCourseLogoUrl } from 'course/helper';
Expand All @@ -13,6 +12,7 @@ import { useAppContext } from 'lib/containers/AppContainer';
import { getUrlParameter } from 'lib/helpers/url-helpers';
import useItems from 'lib/hooks/items/useItems';
import useTranslation from 'lib/hooks/useTranslation';
import moment from 'lib/moment';

import NewCourseButton from './components/NewCourseButton';

Expand Down

This file was deleted.

16 changes: 6 additions & 10 deletions client/app/bundles/common/PrivacyPolicyPage/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { lazy, Suspense } from 'react';
import { defineMessages } from 'react-intl';

const PrivacyPolicyPage = lazy(
() =>
import(/* webpackChunkName: "PrivacyPolicyPage" */ './PrivacyPolicyPage'),
);
import MarkdownPage from 'lib/components/core/layouts/MarkdownPage';

import privacyPolicy from './privacy-policy.md';

const translations = defineMessages({
privacyPolicy: {
Expand All @@ -13,12 +11,10 @@ const translations = defineMessages({
},
});

const SuspensedPrivacyPolicyPage = (): JSX.Element => (
<Suspense>
<PrivacyPolicyPage />
</Suspense>
const PrivacyPolicyPage = (): JSX.Element => (
<MarkdownPage className="m-auto max-w-7xl" markdown={privacyPolicy} />
);

const handle = translations.privacyPolicy;

export default Object.assign(SuspensedPrivacyPolicyPage, { handle });
export default Object.assign(PrivacyPolicyPage, { handle });

This file was deleted.

16 changes: 6 additions & 10 deletions client/app/bundles/common/TermsOfServicePage/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { lazy, Suspense } from 'react';
import { defineMessages } from 'react-intl';

const TermsOfServicePage = lazy(
() =>
import(/* webpackChunkName: "TermsOfServicePage" */ './TermsOfServicePage'),
);
import MarkdownPage from 'lib/components/core/layouts/MarkdownPage';

import termsOfService from './terms-of-service.md';

const translations = defineMessages({
termsOfService: {
Expand All @@ -13,12 +11,10 @@ const translations = defineMessages({
},
});

const SuspensedTermsOfServicePage = (): JSX.Element => (
<Suspense>
<TermsOfServicePage />
</Suspense>
const TermsOfServicePage = (): JSX.Element => (
<MarkdownPage className="m-auto max-w-7xl" markdown={termsOfService} />
);

const handle = translations.termsOfService;

export default Object.assign(SuspensedTermsOfServicePage, { handle });
export default Object.assign(TermsOfServicePage, { handle });
Original file line number Diff line number Diff line change
@@ -1,32 +1,24 @@
import { useRef, useState } from 'react';
import { defineMessages } from 'react-intl';
import { Button } from '@mui/material';
import { LoadingButton } from '@mui/lab';

import CourseAPI from 'api/course';
import toast from 'lib/hooks/toast';
import useTranslation from 'lib/hooks/useTranslation';

require('jquery-ui/ui/widgets/sortable');

interface AchievementReorderingProps {
handleReordering: (state: boolean) => void;
isReordering: boolean;
}

const styles = {
AchievementReorderingButton: {
fontSize: 14,
marginRight: 12,
},
};

const translations = defineMessages({
startReorderAchievement: {
id: 'course.achievement.AchievementReordering.startReorderAchievement',
defaultMessage: 'Reorder',
},
endReorderAchievement: {
id: 'course.achievement.AchievementReordering.endReorderAchievement',
defaultMessage: 'Save New Ordering',
defaultMessage: 'Done reordering',
},
updateFailed: {
id: 'course.achievement.AchievementReordering.updateFailed',
Expand All @@ -38,12 +30,6 @@ const translations = defineMessages({
},
});

// Serialise the ordered achievements as data for the API call.
function serializedOrdering(): string {
const options = { attribute: 'achievementid', key: 'achievement_order[]' };
return $('tbody').first().sortable('serialize', options);
}

const AchievementReordering = (
props: AchievementReorderingProps,
): JSX.Element => {
Expand All @@ -60,35 +46,80 @@ const AchievementReordering = (
}
}

const [loadingSortable, setLoadingSortable] = useState(false);

const sortableCallbacksRef = useRef<{
enable: () => void;
disable: () => void;
}>();

return (
<Button
key="achievement-reordering-button"
className="achievement-reordering-button"
<LoadingButton
color="primary"
loading={loadingSortable}
loadingPosition="start"
onClick={(): void => {
if (loadingSortable) return;

if (!sortableCallbacksRef.current) {
setLoadingSortable(true);

(async (): Promise<void> => {
const [jquery] = await Promise.all([
import(
/* webpackChunkName: "jquery-sortable" */
'jquery'
),
import(
/* webpackChunkName: "jquery-sortable" */
'jquery-ui/ui/widgets/sortable'
),
]);

sortableCallbacksRef.current = {
enable: (): void => {
const table = jquery.default('tbody').first();

table.sortable({
disabled: false,
update() {
const ordering = table.sortable('serialize', {
attribute: 'achievementid',
key: 'achievement_order[]',
});

submitReordering(ordering);
},
});

handleReordering(true);
},
disable: (): void => {
jquery.default('tbody').first().sortable({ disabled: true });
handleReordering(false);
},
};

sortableCallbacksRef.current.enable();

setLoadingSortable(false);
})();

return;
}

if (isReordering) {
$('tbody').first().sortable({ disabled: true });
handleReordering(false);
sortableCallbacksRef.current.disable();
} else {
$('tbody')
.first()
.sortable({
update() {
const ordering = serializedOrdering();
submitReordering(ordering);
},
disabled: false,
});
handleReordering(true);
sortableCallbacksRef.current.enable();
}
}}
style={styles.AchievementReorderingButton}
variant={isReordering ? 'contained' : 'outlined'}
>
{isReordering
? t(translations.endReorderAchievement)
: t(translations.startReorderAchievement)}
</Button>
</LoadingButton>
);
};

Expand Down
Loading