Skip to content

Commit

Permalink
build(webpack): code-split bundles and add content-hash to output fil…
Browse files Browse the repository at this point in the history
…es (#588) (#596)

* build(bundle): add contenth hash to chunks

* build(webpack): lazy load routes

* fix(suspense): move Suspense to approriate place

* build(webpack): prefetch bundles

* build(webpack): exclude css from prefetch groups

* build(webpack): exclude app and npm bundle

* build(webpack): only prefetch lazy-load custom modules

(cherry picked from commit ea89802)

Co-authored-by: Thuan Vo <[email protected]>
  • Loading branch information
mergify[bot] and tthvo authored Oct 31, 2022
1 parent 64a1f0a commit 6e84a1c
Show file tree
Hide file tree
Showing 19 changed files with 122 additions and 65 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"prettier:check": "prettier --check './src/**/*.{tsx,ts}'",
"prettier:apply": "prettier --write './src/**/*.{tsx,ts}'",
"build:bundle-profile": "webpack --config webpack.prod.js --profile --json > stats.json",
"build:bundle-analyze": "webpack-bundle-analyzer ./stats.json",
"build:bundle-analyze": "webpack-bundle-analyzer ./stats.json dist",
"build:bundle-profile-analyze": "npm-run-all -l build:bundle-profile build:bundle-analyze",
"yarn:install": "yarn install",
"yarn:frzinstall": "yarn install --frozen-lockfile"
Expand All @@ -43,6 +43,7 @@
"@types/victory": "^33.1.5",
"@typescript-eslint/eslint-plugin": "^4.32.0",
"@typescript-eslint/parser": "^4.32.0",
"@vue/preload-webpack-plugin": "^2.0.0",
"css-loader": "^5.2.6",
"css-minimizer-webpack-plugin": "^4.0.0",
"dotenv-webpack": "^7.1.0",
Expand Down
6 changes: 5 additions & 1 deletion src/app/About/About.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ import { BreadcrumbPage } from '@app/BreadcrumbPage/BreadcrumbPage';
import { AboutDescription, CRYOSTAT_TRADEMARK } from './AboutDescription';
import { Brand, Card, CardBody, CardFooter, CardHeader } from '@patternfly/react-core';

export const About = () => {
export interface AboutProps {}

export const About: React.FunctionComponent<AboutProps> = (props) => {
return (
<BreadcrumbPage pageTitle="About">
<Card>
Expand All @@ -58,3 +60,5 @@ export const About = () => {
</BreadcrumbPage>
);
};

export default About;
4 changes: 3 additions & 1 deletion src/app/Archives/Archives.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export const uploadAsTarget: Target = {

export interface ArchivesProps {}

export const Archives: React.FunctionComponent<ArchivesProps> = () => {
export const Archives: React.FunctionComponent<ArchivesProps> = (props) => {
const context = React.useContext(ServiceContext);
const addSubscription = useSubscriptions();
const [activeTab, setActiveTab] = React.useState(0);
Expand Down Expand Up @@ -103,3 +103,5 @@ export const Archives: React.FunctionComponent<ArchivesProps> = () => {
</BreadcrumbPage>
);
};

export default Archives;
1 change: 1 addition & 0 deletions src/app/CreateRecording/CreateRecording.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,4 @@ const Comp: React.FunctionComponent<RouteComponentProps<{}, StaticContext, Creat
};

export const CreateRecording = withRouter(Comp);
export default CreateRecording;
6 changes: 5 additions & 1 deletion src/app/Dashboard/Dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@
import * as React from 'react';
import { TargetView } from '@app/TargetView/TargetView';

export const Dashboard = () => {
export interface DashboardProps {}

export const Dashboard: React.FunctionComponent<DashboardProps> = (props) => {
return <TargetView pageTitle="Dashboard" compactSelect={true} />;
};

export default Dashboard;
2 changes: 2 additions & 0 deletions src/app/Events/Events.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,5 @@ export const Events: React.FunctionComponent<EventsProps> = (props) => {
</>
);
};

export default Events;
6 changes: 5 additions & 1 deletion src/app/LoadingView/LoadingView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@
import * as React from 'react';
import { Bullseye, Spinner } from '@patternfly/react-core';

export const LoadingView: React.FunctionComponent = () => {
export interface LoadingViewProps {}

export const LoadingView: React.FunctionComponent<LoadingViewProps> = (props) => {
return (
<>
<br />
Expand All @@ -48,3 +50,5 @@ export const LoadingView: React.FunctionComponent = () => {
</>
);
};

export default LoadingView;
6 changes: 5 additions & 1 deletion src/app/Login/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ import { NoopAuthForm } from './NoopAuthForm';
import { ConnectionError } from './ConnectionError';
import { AuthMethod } from '@app/Shared/Services/Login.service';

export const Login = () => {
export interface LoginProps {}

export const Login: React.FunctionComponent<LoginProps> = (props) => {
const serviceContext = React.useContext(ServiceContext);
const notifications = React.useContext(NotificationsContext);
const [authMethod, setAuthMethod] = React.useState('');
Expand Down Expand Up @@ -109,3 +111,5 @@ export const Login = () => {
</PageSection>
);
};

export default Login;
6 changes: 4 additions & 2 deletions src/app/NotFound/NotFound.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ import '@app/app.css';
import { MapMarkedAltIcon } from '@patternfly/react-icons';
import { IAppRoute, routes, flatten } from '@app/routes';

const NotFound: React.FunctionComponent = () => {
export interface NotFoundProps {}

export const NotFound: React.FunctionComponent<NotFoundProps> = (props) => {
const cards = flatten(routes)
.filter((route: IAppRoute): boolean => !!route.description)
.sort((a: IAppRoute, b: IAppRoute): number => a.title.localeCompare(b.title))
Expand Down Expand Up @@ -80,4 +82,4 @@ const NotFound: React.FunctionComponent = () => {
);
};

export { NotFound };
export default NotFound;
6 changes: 5 additions & 1 deletion src/app/Recordings/Recordings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ import { ActiveRecordingsTable } from './ActiveRecordingsTable';
import { ArchivedRecordingsTable } from './ArchivedRecordingsTable';
import { useSubscriptions } from '@app/utils/useSubscriptions';

export const Recordings = () => {
export interface RecordingsProps {}

export const Recordings: React.FunctionComponent<RecordingsProps> = (props) => {
const context = React.useContext(ServiceContext);
const [activeTab, setActiveTab] = React.useState(0);
const [archiveEnabled, setArchiveEnabled] = React.useState(false);
Expand Down Expand Up @@ -81,3 +83,5 @@ export const Recordings = () => {
</TargetView>
);
};

export default Recordings;
2 changes: 2 additions & 0 deletions src/app/Rules/CreateRule.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -553,3 +553,5 @@ const Comp = () => {
};

export const CreateRule = withRouter(Comp);

export default CreateRule;
6 changes: 5 additions & 1 deletion src/app/Rules/Rules.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,9 @@ export interface Rule {
maxSizeBytes: number;
}

export const Rules = () => {
export interface RulesProps {}

export const Rules: React.FunctionComponent<RulesProps> = (props) => {
const context = React.useContext(ServiceContext);
const routerHistory = useHistory();
const addSubscription = useSubscriptions();
Expand Down Expand Up @@ -436,3 +438,5 @@ export const Rules = () => {
</>
);
};

export default Rules;
6 changes: 5 additions & 1 deletion src/app/SecurityPanel/SecurityPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ import { BreadcrumbPage } from '@app/BreadcrumbPage/BreadcrumbPage';
import { StoreJmxCredentialsCard } from './Credentials/StoreJmxCredentials';
import { ImportCertificate } from './ImportCertificate';

export const SecurityPanel = () => {
export interface SecurityPanelProps {}

export const SecurityPanel: React.FunctionComponent<SecurityPanelProps> = (props) => {
const securityCards = [ImportCertificate, StoreJmxCredentialsCard].map((c) => ({
title: c.title,
description: c.description,
Expand All @@ -63,6 +65,8 @@ export const SecurityPanel = () => {
);
};

export default SecurityPanel;

export interface SecurityCard {
title: string;
description: JSX.Element | string;
Expand Down
6 changes: 5 additions & 1 deletion src/app/Settings/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ import { DeletionDialogControl } from './DeletionDialogControl';
import { WebSocketDebounce } from './WebSocketDebounce';
import { AutoRefresh } from './AutoRefresh';

export const Settings: React.FunctionComponent<{}> = () => {
export interface SettingsProps {}

export const Settings: React.FunctionComponent<SettingsProps> = (props) => {
const settings = [NotificationControl, CredentialsStorage, DeletionDialogControl, WebSocketDebounce, AutoRefresh].map(
(c) => ({
title: c.title,
Expand All @@ -71,6 +73,8 @@ export const Settings: React.FunctionComponent<{}> = () => {
);
};

export default Settings;

export interface UserSetting {
title: string;
description: JSX.Element | string;
Expand Down
83 changes: 46 additions & 37 deletions src/app/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,25 +35,28 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import * as React from 'react';
import { CreateRecording } from '@app/CreateRecording/CreateRecording';
import { Dashboard } from '@app/Dashboard/Dashboard';
import { Events } from '@app/Events/Events';
import { Login } from '@app/Login/Login';
import { NotFound } from '@app/NotFound/NotFound';
import { Recordings } from '@app/Recordings/Recordings';
import { Archives } from '@app/Archives/Archives';
import { Rules } from '@app/Rules/Rules';
import { CreateRule } from '@app/Rules/CreateRule';
import { Settings } from '@app/Settings/Settings';
import { SecurityPanel } from '@app/SecurityPanel/SecurityPanel';

import React, { lazy, Suspense } from 'react';
const CreateRecording = lazy(() => import('@app/CreateRecording/CreateRecording'));
const Dashboard = lazy(() => import('@app/Dashboard/Dashboard'));
const Events = lazy(() => import('@app/Events/Events'));
const Login = lazy(() => import('@app/Login/Login'));
const NotFound = lazy(() => import('@app/NotFound/NotFound'));
const Recordings = lazy(() => import('@app/Recordings/Recordings'));
const Archives = lazy(() => import('@app/Archives/Archives'));
const Rules = lazy(() => import('@app/Rules/Rules'));
const CreateRule = lazy(() => import('@app/Rules/CreateRule'));
const Settings = lazy(() => import('@app/Settings/Settings'));
const SecurityPanel = lazy(() => import('@app/SecurityPanel/SecurityPanel'));
const About = lazy(() => import('@app/About/About'));
import { ServiceContext } from '@app/Shared/Services/Services';
import { useDocumentTitle } from '@app/utils/useDocumentTitle';
import { accessibleRouteChangeHandler } from '@app/utils/utils';
import { Route, RouteComponentProps, Switch } from 'react-router-dom';
import { LastLocationProvider, useLastLocation } from 'react-router-last-location';
import { About } from './About/About';
import { SessionState } from './Shared/Services/Login.service';
import { useSubscriptions } from './utils/useSubscriptions';
import LoadingView from './LoadingView/LoadingView';

let routeFocusTimer: number;
const OVERVIEW = 'Overview';
Expand All @@ -68,7 +71,7 @@ export interface IAppRoute {
exact?: boolean;
path: string;
title: string;
description?: string; //non-empty description is used to filter routes for the NotFound page
description?: string; // non-empty description is used to filter routes for the NotFound page
isAsync?: boolean;
navGroup?: string;
children?: IAppRoute[];
Expand Down Expand Up @@ -206,36 +209,42 @@ const PageNotFound = ({ title }: { title: string }) => {
return <Route component={NotFound} />;
};

const AppRoutes = () => {
export interface AppRoutesProps {}

const AppRoutes: React.FunctionComponent<AppRoutesProps> = (props) => {
const context = React.useContext(ServiceContext);
const addSubscription = useSubscriptions();
const [showDashboard, setShowDashboard] = React.useState(false);

React.useEffect(() => {
const sub = context.login
.getSessionState()
.subscribe((sessionState) => setShowDashboard(sessionState === SessionState.USER_SESSION));
return () => sub.unsubscribe();
}, [context, context.login, setShowDashboard]);
addSubscription(
context.login
.getSessionState()
.subscribe((sessionState) => setShowDashboard(sessionState === SessionState.USER_SESSION))
);
}, [addSubscription, context.login, setShowDashboard]);

return (
<LastLocationProvider>
<Switch>
{showDashboard ? (
flatten(routes).map(({ path, exact, component, title, isAsync }, idx) => (
<RouteWithTitleUpdates
path={path}
exact={exact}
component={component}
key={idx}
title={title}
isAsync={isAsync}
/>
))
) : (
<Login />
)}
<PageNotFound title="404 Page Not Found" />
</Switch>
<Suspense fallback={<LoadingView />}>
<Switch>
{showDashboard ? (
flatten(routes).map(({ path, exact, component, title, isAsync }, idx) => (
<RouteWithTitleUpdates
path={path}
exact={exact}
component={component}
key={idx}
title={title}
isAsync={isAsync}
/>
))
) : (
<Login />
)}
<PageNotFound title="404 Page Not Found" />
</Switch>
</Suspense>
</LastLocationProvider>
);
};
Expand Down
19 changes: 15 additions & 4 deletions webpack.common.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
const ForkTsCheckerPlugin = require('fork-ts-checker-webpack-plugin');
const PreloadWebpackPlugin = require('@vue/preload-webpack-plugin');

const BG_IMAGES_DIRNAME = 'bgimages';
const ASSET_PATH = process.env.ASSET_PATH || '/';

module.exports = env => {
module.exports = (env) => {
return {
context: __dirname,
entry: {
Expand All @@ -18,12 +19,19 @@ module.exports = env => {
template: path.resolve(__dirname, 'src', 'index.html'),
favicon: './src/app/assets/favicon.ico',
}),
new PreloadWebpackPlugin({
rel: 'prefetch',
include: [/\.js$/], // lazy-load chunks to prefetch
// exlude initial chunk, npm chunks
fileBlacklist: [/^(app|npm)(\.[\w-]+)+\.bundle\.js$/]
})
],
// https://medium.com/hackernoon/the-100-correct-way-to-split-your-chunks-with-webpack-f8a9df5b7758
optimization: {
runtimeChunk: 'single',
runtimeChunk: 'single', // create a runtime file to be shared for all generated chunks
moduleIds: 'deterministic', // avoid changing module.id of vendor bundles: https://webpack.js.org/guides/caching/#module-identifiers
splitChunks: {
chunks: 'all',
chunks: 'all', // https://webpack.js.org/plugins/split-chunks-plugin/#splitchunkschunks
maxInitialRequests: Infinity,
minSize: 0,
cacheGroups: {
Expand Down Expand Up @@ -145,9 +153,12 @@ module.exports = env => {
]
},
output: {
filename: '[name].bundle.js',
filename: '[name].[contenthash].bundle.js',
chunkFilename: '[id].[contenthash].bundle.js', // lazy-load modules
hashFunction: "xxhash64",
path: path.resolve(__dirname, 'dist'),
publicPath: ASSET_PATH,
clean: true
},
resolve: {
extensions: ['.ts', '.tsx', '.js', '.jsx'],
Expand Down
5 changes: 0 additions & 5 deletions webpack.dev.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,6 @@ module.exports = merge(common('development'), {
plugins: [
new DotenvPlugin(),
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
hashFunction: "xxhash64",
},
module: {
rules: [
{
Expand Down
Loading

0 comments on commit 6e84a1c

Please sign in to comment.