Code split routes, reduce the initial bundle size + loading times (#1277)

This commit is contained in:
Ryan Clark 2022-11-15 11:15:06 +01:00 committed by GitHub
parent 1b31ef07ab
commit 83aa06791f
14 changed files with 216 additions and 77 deletions

View file

@ -158,6 +158,16 @@ Going to `dumper.go.teleport`, comparing the `Host` header (`dumper.go.teleport`
Note: this only works for local Teleport instances, and won't work for Cloud.
#### Analyzing Webpack's bundle output
To see what is being included in each bundle via [webpack-bundle-analyzer](https://github.com/webpack-contrib/webpack-bundle-analyzer), you can set `WEBPACK_ANALYZE_BUNDLE` to `true` to have it running at `localhost:8888`.
```
$ WEBPACK_ANALYZE_BUNDLE=true yarn start-teleport --target=https://example.com:3080/web
```
And then go to http://localhost:8888.
### Unit-Tests
We use [jest](https://jestjs.io/) as our testing framework.

View file

@ -53,6 +53,13 @@ module.exports = {
['babel-plugin-styled-components', { displayName: true, ssr: false }],
],
},
production: {
plugins: [
...plugins,
['babel-plugin-styled-components', { displayName: false, ssr: false }],
'babel-plugin-lodash',
],
},
},
presets: makePresets(),
plugins: [

View file

@ -42,6 +42,7 @@
"@typescript-eslint/eslint-plugin": "^5.4.0",
"@typescript-eslint/parser": "^5.4.0",
"babel-loader": "^8.2.5",
"babel-plugin-lodash": "^3.3.4",
"clean-webpack-plugin": "4.0.0",
"core-js": "^3",
"cross-env": "5.0.5",
@ -62,6 +63,7 @@
"html-webpack-plugin": "^5.5.0",
"jest": "^27.3.1",
"jest-styled-components": "^7.0.8",
"lodash-webpack-plugin": "^0.11.6",
"msw": "^0.47.4",
"optimist": "^0.6.1",
"prettier": "^2.5.0",
@ -76,7 +78,7 @@
"ts-loader": "^9.3.1",
"typescript": "^4.7.4",
"webpack": "^5.73.0",
"webpack-bundle-analyzer": "^4.5.0",
"webpack-bundle-analyzer": "^4.6.1",
"webpack-cli": "^4.10.0",
"webpack-dev-server": "^4.9.3"
}

View file

@ -16,7 +16,9 @@ limitations under the License.
const path = require('path');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
const HtmlWebPackPlugin = require('html-webpack-plugin');
const LodashModuleReplacementPlugin = require('lodash-webpack-plugin');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
const ReactRefreshPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
@ -49,6 +51,12 @@ const configFactory = {
...options,
});
},
lodash() {
return new LodashModuleReplacementPlugin();
},
bundleAnalyzer(options) {
return new BundleAnalyzerPlugin({ analyzerHost: '0.0.0.0', ...options });
},
},
rules: {
raw() {
@ -64,11 +72,6 @@ const configFactory = {
generator: {
filename: 'assets/fonts/[name][ext]',
},
parser: {
dataUrlCondition: {
maxSize: 102400, // 100kb
},
},
};
},
svg() {
@ -122,31 +125,6 @@ const configFactory = {
/** @return {import('webpack').webpack.Configuration} */
function createDefaultConfig() {
return {
optimization: {
splitChunks: {
cacheGroups: {
// Vendor chunk creates a chunk file that contains files coming from import statements
// from node_modules. The 'initial' flag directs this group to add modules to this chunk
// that were imported inside only from sync chunks.
defaultVendors: {
chunks: 'initial',
name: 'vendor',
test: /([\\/]node_modules[\\/])/,
// Priority states that if a module falls under many cacheGroups, then
// the module will be part of a chunk with a higher priority.
},
// Common chunk creates a chunk file that contains modules that were shared between
// at least 2 (or more) async chunks. The 'async' flag directs this group to add modules
// to this chunk that were specifically imported inside async chunks (dynamic imports).
common: {
chunks: 'async',
minChunks: 2,
test: /([\\/]node_modules[\\/])/,
},
},
},
},
entry: {
app: ['./src/boot'],
},

View file

@ -35,6 +35,15 @@ function getCacheConfig() {
return cache;
}
const plugins = [
configFactory.plugins.tsChecker(),
configFactory.plugins.reactRefresh(),
];
if (process.env.WEBPACK_ANALYZE_BUNDLE === 'true') {
plugins.push(configFactory.plugins.bundleAnalyzer());
}
/**
* @type { import('webpack').webpack.Configuration }
*/
@ -48,10 +57,7 @@ module.exports = {
},
devtool: process.env.WEBPACK_SOURCE_MAP || 'eval-source-map',
mode: 'development',
plugins: [
configFactory.plugins.tsChecker(),
configFactory.plugins.reactRefresh(),
],
plugins,
module: {
strictExportPresence: true,
rules: [

View file

@ -19,6 +19,12 @@ const configFactory = require('./webpack.base');
process.env.BABEL_ENV = 'production';
process.env.NODE_ENV = 'production';
const plugins = [configFactory.plugins.lodash()];
if (process.env.WEBPACK_ANALYZE_BUNDLE === 'true') {
plugins.push(configFactory.plugins.bundleAnalyzer());
}
/**
* @type { import('webpack').webpack.Configuration }
*/
@ -27,6 +33,28 @@ module.exports = {
mode: 'production',
optimization: {
...configFactory.createDefaultConfig().optimization,
runtimeChunk: true,
splitChunks: {
chunks: 'all',
minSize: 20000,
minRemainingSize: 0,
minChunks: 1,
maxAsyncRequests: 30,
maxInitialRequests: 30,
enforceSizeThreshold: 50000,
cacheGroups: {
defaultVendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10,
reuseExistingChunk: true,
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true,
},
},
},
minimize: true,
},
module: {
@ -40,5 +68,5 @@ module.exports = {
configFactory.rules.css(),
],
},
plugins: [],
plugins,
};

View file

@ -13,7 +13,7 @@
"ace-builds": "1.4.6",
"create-react-class": "^15.6.3",
"cross-env": "5.0.5",
"lodash": "^4.17.15",
"lodash": "^4.17.21",
"date-fns": "^2.28.0",
"react": "^16.8.4",
"react-day-picker": "7.3.2",

View file

@ -14,4 +14,5 @@
* limitations under the License.
*/
export { Discover } from './Discover';
// export as default for use with React.lazy
export { Discover as default } from './Discover';

View file

@ -14,8 +14,7 @@
* limitations under the License.
*/
import React from 'react';
const Support = React.lazy(() => import('./Support'));
import Support from './Support';
// export as default for use with React.lazy
export default Support;

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React, { Suspense } from 'react';
import ThemeProvider from 'design/ThemeProvider';
import { Router, Route, Switch } from 'teleport/components/Router';
@ -28,19 +28,17 @@ import { getOSSFeatures } from 'teleport/features';
import { Feature } from 'teleport/types';
import { Main } from './Main';
import Welcome from './Welcome';
import Login, { LoginSuccess, LoginFailed } from './Login';
import AppLauncher from './AppLauncher';
import Console from './Console';
import DesktopSession from './DesktopSession';
import { Discover } from './Discover';
import Player from './Player';
import TeleportContextProvider from './TeleportContextProvider';
import TeleportContext from './teleportContext';
import cfg from './config';
import type { History } from 'history';
const AppLauncher = React.lazy(
() => import(/* webpackChunkName: "app-launcher" */ './AppLauncher')
);
const Teleport: React.FC<Props> = props => {
const { ctx, history } = props;
const publicRoutes = props.renderPublicRoutes || renderPublicRoutes;
@ -76,6 +74,19 @@ const Teleport: React.FC<Props> = props => {
);
};
const LoginFailed = React.lazy(
() => import(/* webpackChunkName: "login-failed" */ './Login/LoginFailed')
);
const LoginSuccess = React.lazy(
() => import(/* webpackChunkName: "login-success" */ './Login/LoginSuccess')
);
const Login = React.lazy(
() => import(/* webpackChunkName: "login" */ './Login')
);
const Welcome = React.lazy(
() => import(/* webpackChunkName: "welcome" */ './Welcome')
);
export function renderPublicRoutes(children = []) {
return [
...children,
@ -113,19 +124,34 @@ export function renderPublicRoutes(children = []) {
];
}
const Console = React.lazy(
() => import(/* webpackChunkName: "console" */ './Console')
);
const Player = React.lazy(
() => import(/* webpackChunkName: "player" */ './Player')
);
const DesktopSession = React.lazy(
() => import(/* webpackChunkName: "desktop-session" */ './DesktopSession')
);
const Discover = React.lazy(
() => import(/* webpackChunkName: "discover" */ './Discover')
);
// TODO: make it lazy loadable
export function renderPrivateRoutes(
CustomMain = Main,
CustomDiscover = Discover
) {
return (
<Switch>
<Route path={cfg.routes.discover} component={CustomDiscover} />
<Route path={cfg.routes.desktop} component={DesktopSession} />
<Route path={cfg.routes.console} component={Console} />
<Route path={cfg.routes.player} component={Player} />
<Route path={cfg.routes.root} component={CustomMain} />
</Switch>
<Suspense fallback={null}>
<Switch>
<Route path={cfg.routes.discover} component={CustomDiscover} />
<Route path={cfg.routes.desktop} component={DesktopSession} />
<Route path={cfg.routes.console} component={Console} />
<Route path={cfg.routes.player} component={Player} />
<Route path={cfg.routes.root} component={CustomMain} />
</Switch>
</Suspense>
);
}

View file

@ -14,27 +14,60 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import * as Icons from 'design/Icon';
import Ctx from 'teleport/teleportContext';
import cfg from 'teleport/config';
import { Feature } from './types';
import Audit from './Audit';
import Nodes from './Nodes';
import Sessions from './Sessions';
import Account from './Account';
import Applications from './Apps';
import Kubes from './Kubes';
import Support from './Support';
import Clusters from './Clusters';
import Trust from './TrustedClusters';
import Users from './Users';
import Roles from './Roles';
import Recordings from './Recordings';
import AuthConnectors from './AuthConnectors';
import Databases from './Databases';
import Desktops from './Desktops';
const Audit = React.lazy(
() => import(/* webpackChunkName: "audit" */ './Audit')
);
const Nodes = React.lazy(
() => import(/* webpackChunkName: "nodes" */ './Nodes')
);
const Sessions = React.lazy(
() => import(/* webpackChunkName: "sessions" */ './Sessions')
);
const Account = React.lazy(
() => import(/* webpackChunkName: "account" */ './Account')
);
const Applications = React.lazy(
() => import(/* webpackChunkName: "apps" */ './Apps')
);
const Kubes = React.lazy(
() => import(/* webpackChunkName: "kubes" */ './Kubes')
);
const Support = React.lazy(
() => import(/* webpackChunkName: "support" */ './Support')
);
const Clusters = React.lazy(
() => import(/* webpackChunkName: "clusters" */ './Clusters')
);
const Trust = React.lazy(
() => import(/* webpackChunkName: "trusted-clusters" */ './TrustedClusters')
);
const Users = React.lazy(
() => import(/* webpackChunkName: "users" */ './Users')
);
const Roles = React.lazy(
() => import(/* webpackChunkName: "roles" */ './Roles')
);
const Recordings = React.lazy(
() => import(/* webpackChunkName: "recordings" */ './Recordings')
);
const AuthConnectors = React.lazy(
() => import(/* webpackChunkName: "auth-connectors" */ './AuthConnectors')
);
const Databases = React.lazy(
() => import(/* webpackChunkName: "databases" */ './Databases')
);
const Desktops = React.lazy(
() => import(/* webpackChunkName: "desktop" */ './Desktops')
);
export class FeatureClusters extends Feature {
topNavTitle = 'Clusters';

View file

@ -11,7 +11,7 @@ module.exports = {
...defaultProdConfig,
optimization: {
...defaultProdConfig.optimization,
moduleIds: 'hashed',
moduleIds: 'deterministic',
},
plugins: [
...defaultProdConfig.plugins,

@ -1 +1 @@
Subproject commit bce332576780c232c89965a1d2376590b4c6e051
Subproject commit 3df4e7e8e050fa3b04f2c3836559c5c537b49f77

View file

@ -202,6 +202,13 @@
dependencies:
"@babel/types" "^7.16.0"
"@babel/helper-module-imports@^7.0.0-beta.49":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz#1e3ebdbbd08aad1437b428c50204db13c5a3ca6e"
integrity sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==
dependencies:
"@babel/types" "^7.18.6"
"@babel/helper-module-transforms@^7.12.1", "@babel/helper-module-transforms@^7.16.0":
version "7.16.0"
resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.16.0.tgz#1c82a8dd4cb34577502ebd2909699b194c3e9bb5"
@ -273,6 +280,11 @@
dependencies:
"@babel/types" "^7.16.0"
"@babel/helper-string-parser@^7.19.4":
version "7.19.4"
resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz#38d3acb654b4701a9b77fb0615a96f775c3a9e63"
integrity sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==
"@babel/helper-validator-identifier@^7.15.7":
version "7.15.7"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz#220df993bfe904a4a6b02ab4f3385a5ebf6e2389"
@ -283,6 +295,11 @@
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz#9c97e30d31b2b8c72a1d08984f2ca9b574d7a076"
integrity sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g==
"@babel/helper-validator-identifier@^7.19.1":
version "7.19.1"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2"
integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==
"@babel/helper-validator-option@^7.14.5":
version "7.14.5"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.14.5.tgz#6e72a1fff18d5dfcb878e1e62f1a021c4b72d5a3"
@ -1138,6 +1155,15 @@
"@babel/helper-validator-identifier" "^7.15.7"
to-fast-properties "^2.0.0"
"@babel/types@^7.0.0-beta.49", "@babel/types@^7.18.6":
version "7.19.4"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.19.4.tgz#0dd5c91c573a202d600490a35b33246fed8a41c7"
integrity sha512-M5LK7nAeS6+9j7hAq+b3fQs+pNfUtTGq+yFFfHnauFA8zQtLRfmuipmsKDKKLuyG+wC8ABW43A153YNawNTEtw==
dependencies:
"@babel/helper-string-parser" "^7.19.4"
"@babel/helper-validator-identifier" "^7.19.1"
to-fast-properties "^2.0.0"
"@bcoe/v8-coverage@^0.2.3":
version "0.2.3"
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
@ -4276,6 +4302,17 @@ babel-plugin-jest-hoist@^27.4.0:
"@types/babel__core" "^7.0.0"
"@types/babel__traverse" "^7.0.6"
babel-plugin-lodash@^3.3.4:
version "3.3.4"
resolved "https://registry.yarnpkg.com/babel-plugin-lodash/-/babel-plugin-lodash-3.3.4.tgz#4f6844358a1340baed182adbeffa8df9967bc196"
integrity sha512-yDZLjK7TCkWl1gpBeBGmuaDIFhZKmkoL+Cu2MUUjv5VxUZx/z7tBGBCBcQs5RI1Bkz5LLmNdjx7paOyQtMovyg==
dependencies:
"@babel/helper-module-imports" "^7.0.0-beta.49"
"@babel/types" "^7.0.0-beta.49"
glob "^7.1.1"
lodash "^4.17.10"
require-package-name "^2.0.1"
babel-plugin-macros@^2.0.0, babel-plugin-macros@^2.8.0:
version "2.8.0"
resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz#0f958a7cc6556b1e65344465d99111a1e5e10138"
@ -10172,6 +10209,13 @@ locate-path@^6.0.0:
dependencies:
p-locate "^5.0.0"
lodash-webpack-plugin@^0.11.6:
version "0.11.6"
resolved "https://registry.yarnpkg.com/lodash-webpack-plugin/-/lodash-webpack-plugin-0.11.6.tgz#8204c6b78beb62ce5211217dfe783c21557ecd33"
integrity sha512-nsHN/+IxZK/C425vGC8pAxkKJ8KQH2+NJnhDul14zYNWr6HJcA95w+oRR7Cp0oZpOdMplDZXmjVROp8prPk7ig==
dependencies:
lodash "^4.17.20"
lodash.camelcase@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6"
@ -12848,6 +12892,11 @@ require-from-string@^2.0.2:
resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909"
integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==
require-package-name@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/require-package-name/-/require-package-name-2.0.1.tgz#c11e97276b65b8e2923f75dabf5fb2ef0c3841b9"
integrity sha512-uuoJ1hU/k6M0779t3VMVIYpb2VMJk05cehCaABFhXaibcbvfgR8wKiozLjVFSzJPmQMRqIcO0HMyTFqfV09V6Q==
requireindex@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/requireindex/-/requireindex-1.2.0.tgz#3463cdb22ee151902635aa6c9535d4de9c2ef1ef"
@ -14989,10 +15038,10 @@ webidl-conversions@^6.1.0:
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-6.1.0.tgz#9111b4d7ea80acd40f5270d666621afa78b69514"
integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==
webpack-bundle-analyzer@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.5.0.tgz#1b0eea2947e73528754a6f9af3e91b2b6e0f79d5"
integrity sha512-GUMZlM3SKwS8Z+CKeIFx7CVoHn3dXFcUAjT/dcZQQmfSZGvitPfMob2ipjai7ovFFqPvTqkEZ/leL4O0YOdAYQ==
webpack-bundle-analyzer@^4.6.1:
version "4.6.1"
resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.6.1.tgz#bee2ee05f4ba4ed430e4831a319126bb4ed9f5a6"
integrity sha512-oKz9Oz9j3rUciLNfpGFjOb49/jEpXNmWdVH8Ls//zNcnLlQdTGXQQMsBbb/gR7Zl8WNLxVCq+0Hqbx3zv6twBw==
dependencies:
acorn "^8.0.4"
acorn-walk "^8.0.0"