diff --git a/.babelrc.js b/.babelrc.js index d16398c90c6..060df472c16 100644 --- a/.babelrc.js +++ b/.babelrc.js @@ -22,25 +22,41 @@ if (process.env.NODE_ENV === 'production') { ]); } -module.exports = { - presets: [ +const overrides = [ + { + test: /.*logger\..*/, + sourceType: 'script', + }, +]; + +module.exports = api => { + const env = api.env(); + const useModern = env === 'modern'; + + const presets = [ [ '@babel/preset-env', { targets: { - browsers: [ - 'chrome >= 53', - 'firefox >= 45.0', - 'ie >= 11', - 'edge >= 37', - 'safari >= 9', - 'opera >= 40', - 'op_mini >= 18', - 'Android >= 7', - 'and_chr >= 53', - 'and_ff >= 49', - 'ios_saf >= 10', - ], + ...(useModern + ? { + esmodules: true, + } + : { + browsers: [ + 'chrome >= 53', + 'firefox >= 45.0', + 'ie >= 11', + 'edge >= 37', + 'safari >= 9', + 'opera >= 40', + 'op_mini >= 18', + 'Android >= 7', + 'and_chr >= 53', + 'and_ff >= 49', + 'ios_saf >= 10', + ], + }), node: 'current', }, // analyses code & polyfills only the features that are used, only for the targeted browsers @@ -49,12 +65,11 @@ module.exports = { }, ], '@babel/preset-react', // transform JSX to JS - ], - plugins: plugins, - overrides: [ - { - test: /.*logger\..*/, - sourceType: 'script', - }, - ], + ]; + + return { + presets, + plugins, + overrides, + }; }; diff --git a/docs/JavaScript-Bundling-Strategy.md b/docs/JavaScript-Bundling-Strategy.md index e50c878e3a1..b5fad2f0c4f 100644 --- a/docs/JavaScript-Bundling-Strategy.md +++ b/docs/JavaScript-Bundling-Strategy.md @@ -1,8 +1,46 @@ # JavaScript Bundling -Because we make multiple releases per day with updated application and library (node_module) code we split our client-side JavaScript bundle into multiple chunks to improve cache efficiency so that the amount of cache-invalidated chunks after each deployment is kept to a minimum. +## Differential serving - modern and legacy bundles using module/nomodule scripts + +Simorgh creates 2 client-side JavaScript bundles. The 2 bundles are made of multiple scripts (or chunks) that are prefixed with `legacy.` and `modern.`, for example, `legacy.main-49d0a293.a47dd2b9.js` and `modern.main-49d0a293.abb18c4e.js`. + +### Why create modern and legacy bundles? + +Legacy browsers need the JavaScript we write to be transformed into something the browser is able to understand and often needs polyfills packaged along with it for missing features of the language. This bloats the JavaScript bundle size and reduces performance for modern browsers that do not need as many transformations or polyfills. At the time of writing, roughly 95% of browsers in use have support for ES2017 syntax and understand most of the code we write without using as many transformations or polyfills. By building 2 separate bundles and conditionally loading and executing only 1 we are increasing the performance for roughly 95% of our users while still providing support to legacy browsers. + +### How is this achieved in Simorgh? + +Simorgh will conditionally load and execute all scripts prefixed with either `legacy.` or `modern.` depending on your browser. Currently, Simorgh considers a browser to be modern if it supports ES2017 syntax and transpiles legacy JavaScript to ES5 syntax for browsers such as IE11 and Opera Mini. + +Simorgh uses Webpack to build 2 different client-side bundles. Most of the client-side Webpack configuration is found in `webpack.client.js`. This config is run with a `BUNDLE_TYPE` argument that returns config for a `modern` or `legacy` browser. The Webpack config uses config from `.babelrc.js` to provide the appropriate JavaScript transformations and polyfills. `.babelrc.js` also needs to dynamically return modern or legacy config but does so using the `process.ENV` variable that is conditionally set using `envName` in the `babel-loader` options in the Webpack config. + +Now that we have the mechanism for generating 2 separate bundles we need to include them in the HTML document. Both modern and legacy bundles need added to the document but the conditional loading and executing is handled using the [module/nomodule](https://3perf.com/blog/polyfills/#modulenomodule) pattern. For example: + +```html + + -### Our strategy + + +``` + +Simorgh uses Loadable Components (a library Simorgh uses for code-splitting) to handle generating script elements. On the server-side, Loadable Components analyses 2 stats files (modern and legacy) generated by Webpack so it can generate the script elements needed in the HTML document. On the client-side, the Loadable Components library queries the DOM for a json script tag (by tag ID `legacy__LOADABLE_REQUIRED_CHUNKS__` or `modern__LOADABLE_REQUIRED_CHUNKS__` which is set using the `namespace` option on the server-side) that contains the JavaScript chunk IDs that Loadable Components will asynchronously load. + +### Gotchas + +- Safari 10.1 supports modules, but does not support the `nomodule` attribute. This results in Safari downloading and executing both legacy and modern bundles. A polyfill has been included to prevent this behaviour. +- IE11 downloads both modern and legacy bundles but only executes the legacy bundle. This worsens performance for IE11 users because they will have to download almost twice the amount of JavaScript. IE11 accounts for around 0.06% of page visits across the World Service sites. Based on this we decided the impact is not high enough to prevent us from providing a better experience to the vast majority of users. +- The Webpack dev server run using `yarn dev` currently only uses modern JavaScript. If you are cross-browser testing locally make sure that you build Simorgh with `yarn build` and start the Express server with `yarn start`. +- The bundle analyser script that runs after builds and displays bundle size information by default will run on the modern bundle. If you would like to see bundle size information for the legacy bundle you can run a build (`yarn build`) and then run `bundleType=legacy node scripts/bundleSize`. + +### More on legacy vs modern bundles + +- [Publish, ship, and install modern JavaScript for faster applications](https://web.dev/publish-modern-javascript/) +- [Deploying ES2015+ Code in Production Today](https://philipwalton.com/articles/deploying-es2015-code-in-production-today/) + +## Code-splitting + +Because we make multiple releases per day with updated application and library (node_module) code we split our client-side JavaScript bundle into multiple chunks to improve cache efficiency so that the amount of cache-invalidated chunks after each deployment is kept to a minimum. Currently, our chunking strategy is as follows: diff --git a/package.json b/package.json index 099a1a9ea08..fcc63a454a5 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "scripts": { "amp:validate": "wait-on -t 20000 http://localhost:7080/status && node ./scripts/ampHtmlValidator/cli.js", "build:local": "rm -rf build && cp envConfig/local.env .env && NODE_ENV=production webpack", - "build": "yarn build:local && node ./scripts/bundleSize/index.js", + "build": "yarn build:local && node ./scripts/bundleSize", "build:profile": "rm -rf build && cp envConfig/local.env .env && IS_PROD_PROFILE=true NODE_ENV=production webpack", "build:live": "cp envConfig/live.env .env && NODE_ENV=production webpack", "build:live:debug": "rm -rf build && awk '{sub(/LOG_DIR=.+/,\"LOG_DIR='log'\")}1' envConfig/live.env > .env && NODE_ENV=production webpack", @@ -58,8 +58,8 @@ "test:linkey": "node scripts/linkeySetup.js && jest src/app/lib/config/services/*.test.js --verbose true; yarn test:linkey:cleanup", "test:linkey:cleanup": "find src/app/lib/config/services -type f -name '*.test.js' -delete", "updateMinorPatch": "rm -rf node_modules/ && yarn install && npm update && yarn install", - "webpack:dev:client": "NODE_ENV=development webpack serve --hot --env config='client'", - "webpack:dev:server": "wait-on ./build/public/loadable-stats-local.json && NODE_ENV=development webpack --watch --env config='server'" + "webpack:dev:client": "NODE_ENV=development webpack serve --config-name='modern' --hot --env config='client'", + "webpack:dev:server": "wait-on ./build/public/modern-loadable-stats-local.json && NODE_ENV=development webpack --watch --env config='server'" }, "repository": { "type": "git", diff --git a/puppeteer/bundleRequests.test.js b/puppeteer/bundleRequests.test.js index 8ec2683bee7..4baa2b4f64a 100644 --- a/puppeteer/bundleRequests.test.js +++ b/puppeteer/bundleRequests.test.js @@ -79,18 +79,21 @@ describe('Js bundle requests', () => { .forEach(url => { expect(url).toMatch( new RegExp( - `(\\/static\\/js\\/(?:comscore\\/)?(main|framework|commons|shared|${serviceRegex}|frosted_promo|.+Page).+?.js)|(\\/static\\/.+?-lib.+?.js)`, + `(\\/static\\/js\\/(?:comscore\\/)?(modern.)?(main|framework|commons|shared|${serviceRegex}|frosted_promo|.+Page).+?.js)|(\\/static\\/.+?-lib.+?.js)`, 'g', ), ); }); }); - it('loads at least 1 service bundle', () => { + it('loads at least 1 modern service bundle', () => { const serviceRegex = getServiceBundleRegex(config[service].name); const serviceMatches = requests.filter(url => url.match( - new RegExp(`(\\/static\\/js\\/${serviceRegex}.+?.js)`, 'g'), + new RegExp( + `(\\/static\\/js\\/modern.${serviceRegex}.+?.js)`, + 'g', + ), ), ); diff --git a/scripts/bundleSize/__mocks__/pageTypeBundleExtractor.js b/scripts/bundleSize/__mocks__/pageTypeBundleExtractor.js index 9689f6a0d9d..0a5b121d8c9 100644 --- a/scripts/bundleSize/__mocks__/pageTypeBundleExtractor.js +++ b/scripts/bundleSize/__mocks__/pageTypeBundleExtractor.js @@ -1,145 +1,145 @@ const extractBundlesForPageType = pageType => { if (pageType === 'ArticlePage') { return [ - 'ArticlePage-31ecd969.31473c35.js', - 'commons-1111.js', - 'commons-2222.js', - '1111-lib-1111.js', - 'shared-1111.js', - 'shared-3333.js', + 'modern.ArticlePage-31ecd969.31473c35.js', + 'modern.commons-1111.js', + 'modern.commons-2222.js', + 'modern.1111-lib-1111.js', + 'modern.shared-1111.js', + 'modern.shared-3333.js', ]; } if (pageType === 'StoryPage') { return [ - 'StoryPage-31ecd969.ca0d676d.js', - 'commons-1111.js', - 'commons-2222.js', - '1111-lib-1111.js', - 'shared-1111.js', - 'shared-2222.js', + 'modern.StoryPage-31ecd969.ca0d676d.js', + 'modern.commons-1111.js', + 'modern.commons-2222.js', + 'modern.1111-lib-1111.js', + 'modern.shared-1111.js', + 'modern.shared-2222.js', ]; } if (pageType === 'FrontPage') { return [ - 'FrontPage-31ecd969.bbf7a07e.js', - 'commons-1111.js', - 'commons-2222.js', - '1111-lib-1111.js', - 'shared-1111.js', + 'modern.FrontPage-31ecd969.bbf7a07e.js', + 'modern.commons-1111.js', + 'modern.commons-2222.js', + 'modern.1111-lib-1111.js', + 'modern.shared-1111.js', ]; } if (pageType === 'IdxPage') { return [ - 'IdxPage-31ecd969.68b77555.js', - 'commons-1111.js', - 'commons-2222.js', - '1111-lib-1111.js', - 'shared-1111.js', - 'shared-2222.js', - 'shared-3333.js', + 'modern.IdxPage-31ecd969.68b77555.js', + 'modern.commons-1111.js', + 'modern.commons-2222.js', + 'modern.1111-lib-1111.js', + 'modern.shared-1111.js', + 'modern.shared-2222.js', + 'modern.shared-3333.js', ]; } if (pageType === 'LiveRadioPage') { return [ - 'LiveRadioPage-31ecd969.64772a90.js', - 'commons-1111.js', - 'commons-2222.js', - '1111-lib-1111.js', - 'shared-1111.js', - 'shared-2222.js', - 'shared-3333.js', + 'modern.LiveRadioPage-31ecd969.64772a90.js', + 'modern.commons-1111.js', + 'modern.commons-2222.js', + 'modern.1111-lib-1111.js', + 'modern.shared-1111.js', + 'modern.shared-2222.js', + 'modern.shared-3333.js', ]; } if (pageType === 'MediaAssetPage') { return [ - 'MediaAssetPage-88a3c260.b7ec8c9c.js', - 'commons-1111.js', - 'commons-2222.js', - '1111-lib-1111.js', - '3333-lib-2222.js', - 'shared-1111.js', + 'modern.MediaAssetPage-88a3c260.b7ec8c9c.js', + 'modern.commons-1111.js', + 'modern.commons-2222.js', + 'modern.1111-lib-1111.js', + 'modern.3333-lib-2222.js', + 'modern.shared-1111.js', ]; } if (pageType === 'MostReadPage') { return [ - 'MostReadPage-31ecd969.7484ff05.js', - 'commons-1111.js', - 'commons-2222.js', - '1111-lib-1111.js', - 'shared-1111.js', - 'shared-2222.js', - 'shared-3333.js', + 'modern.MostReadPage-31ecd969.7484ff05.js', + 'modern.commons-1111.js', + 'modern.commons-2222.js', + 'modern.1111-lib-1111.js', + 'modern.shared-1111.js', + 'modern.shared-2222.js', + 'modern.shared-3333.js', ]; } if (pageType === 'MostWatchedPage') { return [ - 'MostWatchedPage-31ecd969.7484ff05.js', - 'commons-1111.js', - 'commons-2222.js', - '1111-lib-1111.js', - 'shared-1111.js', - 'shared-2222.js', - 'shared-3333.js', + 'modern.MostWatchedPage-31ecd969.7484ff05.js', + 'modern.commons-1111.js', + 'modern.commons-2222.js', + 'modern.1111-lib-1111.js', + 'modern.shared-1111.js', + 'modern.shared-2222.js', + 'modern.shared-3333.js', ]; } if (pageType === 'OnDemandAudioPage') { return [ - 'OnDemandAudioPage-31ecd969.ec6af2d0.js', - 'commons-1111.js', - 'commons-2222.js', - '1111-lib-1111.js', - 'shared-2222.js', - 'shared-3333.js', + 'modern.OnDemandAudioPage-31ecd969.ec6af2d0.js', + 'modern.commons-1111.js', + 'modern.commons-2222.js', + 'modern.1111-lib-1111.js', + 'modern.shared-2222.js', + 'modern.shared-3333.js', ]; } if (pageType === 'OnDemandTvPage') { return [ - 'OnDemandTvPage-31ecd969.de41ab7f.js', - 'commons-1111.js', - '1111-lib-1111.js', - '3333-lib-2222.js', - 'shared-1111.js', + 'modern.OnDemandTvPage-31ecd969.de41ab7f.js', + 'modern.commons-1111.js', + 'modern.1111-lib-1111.js', + 'modern.3333-lib-2222.js', + 'modern.shared-1111.js', ]; } if (pageType === 'PhotoGalleryPage') { return [ - 'PhotoGalleryPage-e94df663.a733283a.js', - 'commons-1111.js', - 'commons-2222.js', - '1111-lib-1111.js', - 'shared-1111.js', - 'shared-3333.js', + 'modern.PhotoGalleryPage-e94df663.a733283a.js', + 'modern.commons-1111.js', + 'modern.commons-2222.js', + 'modern.1111-lib-1111.js', + 'modern.shared-1111.js', + 'modern.shared-3333.js', ]; } if (pageType === 'ErrorPage') { return [ - 'ErrorPage-31ecd969.31473c35.js', - 'commons-1111.js', - 'commons-2222.js', - '1111-lib-1111.js', - 'shared-1111.js', - 'shared-2222.js', - 'shared-3333.js', + 'modern.ErrorPage-31ecd969.31473c35.js', + 'modern.commons-1111.js', + 'modern.commons-2222.js', + 'modern.1111-lib-1111.js', + 'modern.shared-1111.js', + 'modern.shared-2222.js', + 'modern.shared-3333.js', ]; } if (pageType === 'IdxPage') { return [ - 'IdxPage-31ecd969.31473c35.js', - 'commons-1111.js', - 'commons-2222.js', - '1111-lib-1111.js', - '3333-lib-2222.js', - 'shared-1111.js', + 'modern.IdxPage-31ecd969.31473c35.js', + 'modern.commons-1111.js', + 'modern.commons-2222.js', + 'modern.1111-lib-1111.js', + 'modern.3333-lib-2222.js', + 'modern.shared-1111.js', ]; } if (pageType === 'FeatureIdxPage') { return [ - 'FeatureIdxPage-31ecd969.31473c35.js', - 'commons-1111.js', - 'commons-2222.js', - '1111-lib-1111.js', - '3333-lib-2222.js', - 'shared-1111.js', + 'modern.FeatureIdxPage-31ecd969.31473c35.js', + 'modern.commons-1111.js', + 'modern.commons-2222.js', + 'modern.1111-lib-1111.js', + 'modern.3333-lib-2222.js', + 'modern.shared-1111.js', ]; } diff --git a/scripts/bundleSize/bundleSize.test.js b/scripts/bundleSize/bundleSize.test.js index c42d1e7630c..41f3dc2cf81 100644 --- a/scripts/bundleSize/bundleSize.test.js +++ b/scripts/bundleSize/bundleSize.test.js @@ -26,16 +26,16 @@ jest.mock('./bundleSizeConfig', () => ({ const setUpFSMocks = (service1FileSize, service2FileSize) => { beforeEach(() => { const bundles = [ - 'main-12345.js', - 'service1-12345.12345.js', - 'service2-12345.12345.js', - '1111-lib-1111.js', - 'commons-1111.js', - 'commons-2222.js', - 'commons-3333.js', - 'shared-1111.js', - 'shared-2222.js', - 'framework-1111.js', + 'modern.main-12345.js', + 'modern.service1-12345.12345.js', + 'modern.service2-12345.12345.js', + 'modern.1111-lib-1111.js', + 'modern.commons-1111.js', + 'modern.commons-2222.js', + 'modern.commons-3333.js', + 'modern.shared-1111.js', + 'modern.shared-2222.js', + 'modern.framework-1111.js', ]; readdirSync.mockReturnValue(bundles); @@ -140,7 +140,7 @@ describe('bundleSize', () => { " Results - Service bundles + MODERN service bundle sizes ┌──────────────┬─────────────────────────────────┬─────────────────┐ │ Service name │ bundles │ Total size (kB) │ ├──────────────┼─────────────────────────────────┼─────────────────┤ @@ -149,7 +149,7 @@ describe('bundleSize', () => { │ service2 │ service2-12345.12345.js (146kB) │ 146 │ └──────────────┴─────────────────────────────────┴─────────────────┘ - Service bundles summary + MODERN service bundle sizes summary ┌─────────────────────────────────┬─────┐ │ Smallest total bundle size (kB) │ 142 │ ├─────────────────────────────────┼─────┤ @@ -158,7 +158,7 @@ describe('bundleSize', () => { │ Average total bundle size (kB) │ 144 │ └─────────────────────────────────┴─────┘ - Page type bundles + MODERN page type bundle sizes ┌───────────────────┬──────────────────────────┬──────────────────────────┬──────────────────────────┬──────────────────────────┬──────────────────────────┬──────────────────────────┬─────────────────┐ │ Page type │ main │ framework │ lib │ shared │ commons │ page │ Total size (kB) │ ├───────────────────┼──────────────────────────┼──────────────────────────┼──────────────────────────┼──────────────────────────┼──────────────────────────┼──────────────────────────┼─────────────────┤ @@ -208,7 +208,7 @@ describe('bundleSize', () => { │ │ │ │ │ shared-333…333.js (39kB) │ │ │ │ └───────────────────┴──────────────────────────┴──────────────────────────┴──────────────────────────┴──────────────────────────┴──────────────────────────┴──────────────────────────┴─────────────────┘ - Page bundles summary (excludes service bundle) + MODERN page bundle sizes summary (excludes service bundle) ┌─────────────────────────────────┬─────┐ │ Smallest total bundle size (kB) │ 353 │ ├─────────────────────────────────┼─────┤ @@ -217,7 +217,7 @@ describe('bundleSize', () => { │ Average total bundle size (kB) │ 409 │ └─────────────────────────────────┴─────┘ - Service + Page bundles summary + MODERN service + page bundle sizes summary ┌────────────────────────────────────────────────────────────────────┬─────┐ │ Smallest total bundle size (kB) (smallest service + smallest page) │ 495 │ ├────────────────────────────────────────────────────────────────────┼─────┤ diff --git a/scripts/bundleSize/bundleSizeConfig.js b/scripts/bundleSize/bundleSizeConfig.js index 2478f5f4946..2e043e95dba 100644 --- a/scripts/bundleSize/bundleSizeConfig.js +++ b/scripts/bundleSize/bundleSizeConfig.js @@ -4,6 +4,6 @@ module.exports = { // Keep the MAX_SIZE +5 above the largest value and MIN_SIZE -5 // below the smallest value in the build output; this avoids the // need for frequent changes as bundle sizes fluctuate. - MIN_SIZE: 636, - MAX_SIZE: 952, + MIN_SIZE: 568, + MAX_SIZE: 855, }; diff --git a/scripts/bundleSize/getBundleData.js b/scripts/bundleSize/getBundleData.js index b4915a8273c..f0f4100d10c 100644 --- a/scripts/bundleSize/getBundleData.js +++ b/scripts/bundleSize/getBundleData.js @@ -3,6 +3,7 @@ const { extractBundlesForPageType } = require('./pageTypeBundleExtractor'); // need fake Cypress in global scope to require service configs: global.Cypress = { env: () => ({}) }; +const bundleType = process.env.bundleType || 'modern'; const cypressServiceConfigs = require('../../cypress/support/config/services'); const { pages } = require('./pages'); @@ -27,10 +28,10 @@ const getBundlesData = bundles => const getPageBundleData = () => { const main = getBundlesData( - jsFiles.filter(fileName => fileName.startsWith('main-')), + jsFiles.filter(fileName => fileName.startsWith(`${bundleType}.main-`)), ); const framework = getBundlesData( - jsFiles.filter(fileName => fileName.startsWith('framework')), + jsFiles.filter(fileName => fileName.startsWith(`${bundleType}.framework`)), ); const mainTotalSize = main.reduce((acc, { size }) => acc + size, 0); const frameworkTotalSize = framework.reduce((acc, { size }) => acc + size, 0); @@ -42,9 +43,9 @@ const getPageBundleData = () => { return bundlesData.reduce( ({ lib, shared, page, commons, totalSize, ...rest }, { name, size }) => { const bundleData = { name, size }; - const isShared = name.startsWith('shared-'); - const isLib = name.includes('-lib'); - const isCommons = name.includes('commons-'); + const isShared = new RegExp(`^${bundleType}\\.shared-`).test(name); + const isLib = new RegExp(`^${bundleType}\\..+?-lib`).test(name); + const isCommons = new RegExp(`^${bundleType}\\.commons-`).test(name); if (isLib) { lib.push(bundleData); @@ -82,7 +83,7 @@ const getServiceBundleData = () => services .map(service => { const bundlesData = getBundlesData( - jsFiles.filter(file => file.includes(service)), + jsFiles.filter(file => file.startsWith(`${bundleType}.${service}`)), ); return { serviceName: service, bundles: bundlesData }; diff --git a/scripts/bundleSize/getBundleData.test.js b/scripts/bundleSize/getBundleData.test.js index 7960083e5ee..a1f7920d8c0 100644 --- a/scripts/bundleSize/getBundleData.test.js +++ b/scripts/bundleSize/getBundleData.test.js @@ -4,74 +4,74 @@ import { pages } from './pages'; jest.mock('./pageTypeBundleExtractor'); jest.mock('fs', () => ({ readdirSync: () => [ - 'ArticlePage-31ecd969.31473c35.js', - 'FrontPage-31ecd969.bbf7a07e.js', - 'IdxPage-31ecd969.68b77555.js', - 'LiveRadioPage-31ecd969.64772a90.js', - 'MediaAssetPage-88a3c260.b7ec8c9c.js', - 'MostReadPage-31ecd969.7484ff05.js', - 'MostWatchedPage-31ecd969.7484rr05.js', - 'OnDemandAudioPage-31ecd969.ec6af2d0.js', - 'OnDemandTvPage-31ecd969.de41ab7f.js', - 'PhotoGalleryPage-e94df663.a733283a.js', - 'StoryPage-31ecd969.ca0d676d.js', - 'afaanoromoo-31ecd969.fe0503d1.js', - 'afrique-31ecd969.61a113f0.js', - 'amharic-31ecd969.660e4865.js', - 'arabic-31ecd969.c022dfb0.js', - 'archive-31ecd969.9a6a30fb.js', - 'azeri-31ecd969.ee2579a9.js', - 'bengali-31ecd969.7f2e9af6.js', - 'burmese-31ecd969.6dba80fc.js', - 'commons-1111.js', - 'commons-2222.js', - 'commons-3333.js', - '1111-lib-1111.js', - 'framework-1111.js', - 'shared-1111.js', - 'shared-2222.js', - 'shared-3333.js', - 'cymrufyw-31ecd969.f1e65089.js', - 'gahuza-31ecd969.ba8347e8.js', - 'gujarati-31ecd969.f3443ddd.js', - 'hausa-31ecd969.6a99ac36.js', - 'hindi-31ecd969.b53e968b.js', - 'igbo-31ecd969.be1f5cf9.js', - 'indonesia-31ecd969.27dce298.js', - 'japanese-31ecd969.044eb92d.js', - 'korean-31ecd969.824770cd.js', - 'kyrgyz-31ecd969.82a43555.js', - 'main-d0ae3f07.8d44cc89.js', - 'main-f71cff67.a1021a9a.js', - 'marathi-31ecd969.dbc74afe.js', - 'mundo-31ecd969.82160792.js', - 'naidheachdan-31ecd969.be670b0b.js', - 'nepali-31ecd969.c645b661.js', - 'news-31ecd969.a17a1b73.js', - 'pashto-31ecd969.ff214078.js', - 'persian-31ecd969.16f5dbaa.js', - 'pidgin-31ecd969.6653a4a5.js', - 'portuguese-31ecd969.fbafad11.js', - 'punjabi-31ecd969.246368c7.js', - 'rich-text-transforms-748942c6.8bdcb545.js', - 'russian-31ecd969.497cb64b.js', - 'scotland-31ecd969.fd8e7871.js', - 'serbian-31ecd969.e081af9a.js', - 'sinhala-31ecd969.2ea43cb7.js', - 'somali-31ecd969.4f58537b.js', - 'swahili-31ecd969.80f5048c.js', - 'tamil-31ecd969.313fd37e.js', - 'telugu-31ecd969.93c2eda1.js', - 'thai-31ecd969.9740de23.js', - 'tigrinya-31ecd969.a4b0e358.js', - 'turkce-31ecd969.2fb5f1c7.js', - 'ukchina-31ecd969.448f78e1.js', - 'ukrainian-31ecd969.0b427c1a.js', - 'urdu-31ecd969.cc15ea70.js', - 'uzbek-31ecd969.4dae23cc.js', - 'vietnamese-31ecd969.96b409d0.js', - 'yoruba-31ecd969.2072cb94.js', - 'zhongwen-31ecd969.40328f02.js', + 'modern.ArticlePage-31ecd969.31473c35.js', + 'modern.FrontPage-31ecd969.bbf7a07e.js', + 'modern.IdxPage-31ecd969.68b77555.js', + 'modern.LiveRadioPage-31ecd969.64772a90.js', + 'modern.MediaAssetPage-88a3c260.b7ec8c9c.js', + 'modern.MostReadPage-31ecd969.7484ff05.js', + 'modern.MostWatchedPage-31ecd969.7484rr05.js', + 'modern.OnDemandAudioPage-31ecd969.ec6af2d0.js', + 'modern.OnDemandTvPage-31ecd969.de41ab7f.js', + 'modern.PhotoGalleryPage-e94df663.a733283a.js', + 'modern.StoryPage-31ecd969.ca0d676d.js', + 'modern.afaanoromoo-31ecd969.fe0503d1.js', + 'modern.afrique-31ecd969.61a113f0.js', + 'modern.amharic-31ecd969.660e4865.js', + 'modern.arabic-31ecd969.c022dfb0.js', + 'modern.archive-31ecd969.9a6a30fb.js', + 'modern.azeri-31ecd969.ee2579a9.js', + 'modern.bengali-31ecd969.7f2e9af6.js', + 'modern.burmese-31ecd969.6dba80fc.js', + 'modern.commons-1111.js', + 'modern.commons-2222.js', + 'modern.commons-3333.js', + 'modern.1111-lib-1111.js', + 'modern.framework-1111.js', + 'modern.shared-1111.js', + 'modern.shared-2222.js', + 'modern.shared-3333.js', + 'modern.cymrufyw-31ecd969.f1e65089.js', + 'modern.gahuza-31ecd969.ba8347e8.js', + 'modern.gujarati-31ecd969.f3443ddd.js', + 'modern.hausa-31ecd969.6a99ac36.js', + 'modern.hindi-31ecd969.b53e968b.js', + 'modern.igbo-31ecd969.be1f5cf9.js', + 'modern.indonesia-31ecd969.27dce298.js', + 'modern.japanese-31ecd969.044eb92d.js', + 'modern.korean-31ecd969.824770cd.js', + 'modern.kyrgyz-31ecd969.82a43555.js', + 'modern.main-d0ae3f07.8d44cc89.js', + 'modern.main-f71cff67.a1021a9a.js', + 'modern.marathi-31ecd969.dbc74afe.js', + 'modern.mundo-31ecd969.82160792.js', + 'modern.naidheachdan-31ecd969.be670b0b.js', + 'modern.nepali-31ecd969.c645b661.js', + 'modern.news-31ecd969.a17a1b73.js', + 'modern.pashto-31ecd969.ff214078.js', + 'modern.persian-31ecd969.16f5dbaa.js', + 'modern.pidgin-31ecd969.6653a4a5.js', + 'modern.portuguese-31ecd969.fbafad11.js', + 'modern.punjabi-31ecd969.246368c7.js', + 'modern.rich-text-transforms-748942c6.8bdcb545.js', + 'modern.russian-31ecd969.497cb64b.js', + 'modern.scotland-31ecd969.fd8e7871.js', + 'modern.serbian-31ecd969.e081af9a.js', + 'modern.sinhala-31ecd969.2ea43cb7.js', + 'modern.somali-31ecd969.4f58537b.js', + 'modern.swahili-31ecd969.80f5048c.js', + 'modern.tamil-31ecd969.313fd37e.js', + 'modern.telugu-31ecd969.93c2eda1.js', + 'modern.thai-31ecd969.9740de23.js', + 'modern.tigrinya-31ecd969.a4b0e358.js', + 'modern.turkce-31ecd969.2fb5f1c7.js', + 'modern.ukchina-31ecd969.448f78e1.js', + 'modern.ukrainian-31ecd969.0b427c1a.js', + 'modern.urdu-31ecd969.cc15ea70.js', + 'modern.uzbek-31ecd969.4dae23cc.js', + 'modern.vietnamese-31ecd969.96b409d0.js', + 'modern.yoruba-31ecd969.2072cb94.js', + 'modern.zhongwen-31ecd969.40328f02.js', ], statSync: () => ({ size: 10000 }), @@ -96,50 +96,50 @@ describe('getPageBundleData', () => { Object { "commons": Array [ Object { - "name": "commons-1111.js", + "name": "modern.commons-1111.js", "size": 10, }, Object { - "name": "commons-2222.js", + "name": "modern.commons-2222.js", "size": 10, }, ], "framework": Array [ Object { - "name": "framework-1111.js", + "name": "modern.framework-1111.js", "size": 10, }, ], "lib": Array [ Object { - "name": "1111-lib-1111.js", + "name": "modern.1111-lib-1111.js", "size": 10, }, ], "main": Array [ Object { - "name": "main-d0ae3f07.8d44cc89.js", + "name": "modern.main-d0ae3f07.8d44cc89.js", "size": 10, }, Object { - "name": "main-f71cff67.a1021a9a.js", + "name": "modern.main-f71cff67.a1021a9a.js", "size": 10, }, ], "page": Array [ Object { - "name": "PhotoGalleryPage-e94df663.a733283a.js", + "name": "modern.PhotoGalleryPage-e94df663.a733283a.js", "size": 10, }, ], "pageName": "PhotoGalleryPage", "shared": Array [ Object { - "name": "shared-1111.js", + "name": "modern.shared-1111.js", "size": 10, }, Object { - "name": "shared-3333.js", + "name": "modern.shared-3333.js", "size": 10, }, ], @@ -159,46 +159,46 @@ describe('getPageBundleData', () => { Object { "commons": Array [ Object { - "name": "commons-1111.js", + "name": "modern.commons-1111.js", "size": 10, }, ], "framework": Array [ Object { - "name": "framework-1111.js", + "name": "modern.framework-1111.js", "size": 10, }, ], "lib": Array [ Object { - "name": "1111-lib-1111.js", + "name": "modern.1111-lib-1111.js", "size": 10, }, Object { - "name": "3333-lib-2222.js", + "name": "modern.3333-lib-2222.js", "size": 10, }, ], "main": Array [ Object { - "name": "main-d0ae3f07.8d44cc89.js", + "name": "modern.main-d0ae3f07.8d44cc89.js", "size": 10, }, Object { - "name": "main-f71cff67.a1021a9a.js", + "name": "modern.main-f71cff67.a1021a9a.js", "size": 10, }, ], "page": Array [ Object { - "name": "OnDemandTvPage-31ecd969.de41ab7f.js", + "name": "modern.OnDemandTvPage-31ecd969.de41ab7f.js", "size": 10, }, ], "pageName": "OnDemandTvPage", "shared": Array [ Object { - "name": "shared-1111.js", + "name": "modern.shared-1111.js", "size": 10, }, ], diff --git a/scripts/bundleSize/index.js b/scripts/bundleSize/index.js index b56e0089f4a..fd88119e187 100755 --- a/scripts/bundleSize/index.js +++ b/scripts/bundleSize/index.js @@ -10,6 +10,7 @@ const createConsoleError = require('./createConsoleError'); const { getPageBundleData, getServiceBundleData } = require('./getBundleData'); const { MIN_SIZE, MAX_SIZE } = require('./bundleSizeConfig'); +const bundleType = process.env.bundleType || 'modern'; const serviceBundleData = sortByBundlesTotalAscending(getServiceBundleData()); const serviceBundlesTotals = serviceBundleData.map( ({ totalSize }) => totalSize, @@ -31,6 +32,8 @@ const largestPagePlusServiceBundleSize = const smallestPagePlusServiceBundleSize = smallestServiceBundleSize + smallestPageBundleSize; +const removeBundleTypePrefix = name => name.replace(`${bundleType}.`, ''); + const serviceBundlesTable = new Table({ head: ['Service name', 'bundles', 'Total size (kB)'], }); @@ -51,7 +54,9 @@ const pageBundlesTable = new Table({ pageBundleData.forEach( ({ pageName, main, framework, lib, shared, commons, page, totalSize }) => { const getFileInfo = ({ name, size }) => - `${name.slice(0, 10)}…${name.slice(-6)} (${size}kB)`; + `${removeBundleTypePrefix(name).slice(0, 10)}…${name.slice( + -6, + )} (${size}kB)`; pageBundlesTable.push([ pageName, @@ -67,7 +72,8 @@ pageBundleData.forEach( ); serviceBundleData.forEach(({ serviceName, bundles, totalSize }) => { - const getFileInfo = ({ name, size }) => `${name} (${size}kB)`; + const getFileInfo = ({ name, size }) => + `${removeBundleTypePrefix(name)} (${size}kB)`; serviceBundlesTable.push([ serviceName, @@ -93,13 +99,16 @@ serviceSummaryTable.push( const servicePageSummaryTable = new Table(); servicePageSummaryTable.push( { - 'Smallest total bundle size (kB) (smallest service + smallest page)': smallestPagePlusServiceBundleSize, + 'Smallest total bundle size (kB) (smallest service + smallest page)': + smallestPagePlusServiceBundleSize, }, { - 'Largest total bundle size (kB) (largest service + largest page)': largestPagePlusServiceBundleSize, + 'Largest total bundle size (kB) (largest service + largest page)': + largestPagePlusServiceBundleSize, }, ); +const styledBundleTypeTitle = chalk.green(bundleType.toUpperCase()); const spinner = ora({ text: 'Analysing bundles...', color: 'magenta', @@ -107,24 +116,32 @@ const spinner = ora({ spinner.start(); console.log(chalk.bold('\n\nResults')); -console.log(chalk.bold('\nService bundles\n')); +console.log(chalk.bold(`\n${styledBundleTypeTitle} service bundle sizes\n`)); console.log(serviceBundlesTable.toString()); -console.log(chalk.bold('\n\nService bundles summary\n')); +console.log( + chalk.bold(`\n\n${styledBundleTypeTitle} service bundle sizes summary\n`), +); console.log(serviceSummaryTable.toString()); -console.log(chalk.bold('\n\nPage type bundles\n')); +console.log( + chalk.bold(`\n\n${styledBundleTypeTitle} page type bundle sizes\n`), +); console.log(pageBundlesTable.toString()); console.log( [ - chalk.bold('\n\nPage bundles summary'), + chalk.bold(`\n\n${styledBundleTypeTitle} page bundle sizes summary`), chalk.cyan.bold('(excludes service bundle)\n'), ].join(' '), ); console.log(pageSummaryTable.toString()); -console.log(chalk.bold('\n\nService + Page bundles summary\n')); +console.log( + chalk.bold( + `\n\n${styledBundleTypeTitle} service + page bundle sizes summary\n`, + ), +); console.log(servicePageSummaryTable.toString()); const errors = []; diff --git a/scripts/bundleSize/pageTypeBundleExtractor.js b/scripts/bundleSize/pageTypeBundleExtractor.js index fb0176e28a0..3334c763fbd 100644 --- a/scripts/bundleSize/pageTypeBundleExtractor.js +++ b/scripts/bundleSize/pageTypeBundleExtractor.js @@ -1,7 +1,14 @@ -const bundleReport = require('../../reports/webpackBundleReport.json'); +const modernBundleReport = require('../../reports/modern.webpackBundleReport.json'); +const legacyBundleReport = require('../../reports/legacy.webpackBundleReport.json'); + +const bundleReports = { + modern: modernBundleReport, + legacy: legacyBundleReport, +}; +const bundleType = process.env.bundleType || 'modern'; const extractBundlesForPageType = pageComponent => { - const chunkGroup = bundleReport.namedChunkGroups[pageComponent]; + const chunkGroup = bundleReports[bundleType].namedChunkGroups[pageComponent]; if (chunkGroup) { return chunkGroup.assets .filter(({ name }) => name.endsWith('.js')) diff --git a/scripts/bundleSize/pageTypeBundleExtractor.test.js b/scripts/bundleSize/pageTypeBundleExtractor.test.js index a6fd1c3bea1..30631ff25e6 100644 --- a/scripts/bundleSize/pageTypeBundleExtractor.test.js +++ b/scripts/bundleSize/pageTypeBundleExtractor.test.js @@ -1,22 +1,22 @@ import { extractBundlesForPageType } from './pageTypeBundleExtractor'; jest.mock( - '../../reports/webpackBundleReport.json', + '../../reports/modern.webpackBundleReport.json', () => ({ namedChunkGroups: { main: { chunks: [65, 68, 66, 69, 67], assets: [ - { name: 'static/js/framework-4f9840c1.f9a60e6c.js' }, - { name: 'static/js/framework-4f9840c1.f9a60e6c.js.map' }, - { name: 'static/js/main-d0ae3f07.f40d46f9.js' }, - { name: 'static/js/main-d0ae3f07.f40d46f9.js.map' }, - { name: 'static/js/main-7d359b94.a807aeb0.js' }, - { name: 'static/js/main-7d359b94.a807aeb0.js.map' }, - { name: 'static/js/main-f71cff67.a32d8cd2.js' }, - { name: 'static/js/main-f71cff67.a32d8cd2.js.map' }, - { name: 'static/js/main-88a3c260.517db56f.js' }, - { name: 'static/js/main-88a3c260.517db56f.js.map' }, + { name: 'static/js/modern.framework-4f9840c1.f9a60e6c.js' }, + { name: 'static/js/modern.framework-4f9840c1.f9a60e6c.js.map' }, + { name: 'static/js/modern.main-d0ae3f07.f40d46f9.js' }, + { name: 'static/js/modern.main-d0ae3f07.f40d46f9.js.map' }, + { name: 'static/js/modern.main-7d359b94.a807aeb0.js' }, + { name: 'static/js/modern.main-7d359b94.a807aeb0.js.map' }, + { name: 'static/js/modern.main-f71cff67.a32d8cd2.js' }, + { name: 'static/js/modern.main-f71cff67.a32d8cd2.js.map' }, + { name: 'static/js/modern.main-88a3c260.517db56f.js' }, + { name: 'static/js/modern.main-88a3c260.517db56f.js.map' }, ], children: {}, childAssets: {}, @@ -25,34 +25,30 @@ jest.mock( ArticlePage: { chunks: [4, 0, 1, 2, 3, 5, 6, 53], assets: [ - { name: 'static/js/../moment-lib-87d47d0c.d5f3ec50.js' }, - { name: 'static/js/../moment-lib-87d47d0c.d5f3ec50.js.map' }, - { name: 'static/js/commons-0f485567.d710e458.js' }, - { name: 'static/js/commons-0f485567.d710e458.js.map' }, - { name: 'static/js/commons-7d359b94.ced895c9.js' }, - { name: 'static/js/commons-7d359b94.ced895c9.js.map' }, - { name: 'static/js/commons-8493eda2.7bb97fc0.js' }, - { name: 'static/js/commons-8493eda2.7bb97fc0.js.map' }, - { name: 'static/js/commons-92a4fe01.6f43e2d7.js' }, - { name: 'static/js/commons-92a4fe01.6f43e2d7.js.map' }, + { name: 'static/js/modern.../moment-lib-87d47d0c.d5f3ec50.js' }, + { name: 'static/js/modern.../moment-lib-87d47d0c.d5f3ec50.js.map' }, + { name: 'static/js/modern.commons-0f485567.d710e458.js' }, + { name: 'static/js/modern.commons-0f485567.d710e458.js.map' }, + { name: 'static/js/modern.commons-7d359b94.ced895c9.js' }, + { name: 'static/js/modern.commons-7d359b94.ced895c9.js.map' }, + { name: 'static/js/modern.commons-8493eda2.7bb97fc0.js' }, + { name: 'static/js/modern.commons-8493eda2.7bb97fc0.js.map' }, + { name: 'static/js/modern.commons-92a4fe01.6f43e2d7.js' }, + { name: 'static/js/modern.commons-92a4fe01.6f43e2d7.js.map' }, { - name: - 'static/js/shared-UddsGWzeoXsaLwaRPMwTQELcfA=-31ecd969.898fb3aa.js', + name: 'static/js/modern.shared-UddsGWzeoXsaLwaRPMwTQELcfA=-31ecd969.898fb3aa.js', }, { - name: - 'static/js/shared-UddsGWzeoXsaLwaRPMwTQELcfA=-31ecd969.898fb3aa.js.map', + name: 'static/js/modern.shared-UddsGWzeoXsaLwaRPMwTQELcfA=-31ecd969.898fb3aa.js.map', }, { - name: - 'static/js/shared-nj6qIml+EtJxDVgSunxJydSAIpY=-253ae210.3f8a9c3a.js', + name: 'static/js/modern.shared-nj6qIml+EtJxDVgSunxJydSAIpY=-253ae210.3f8a9c3a.js', }, { - name: - 'static/js/shared-nj6qIml+EtJxDVgSunxJydSAIpY=-253ae210.3f8a9c3a.js.map', + name: 'static/js/modern.shared-nj6qIml+EtJxDVgSunxJydSAIpY=-253ae210.3f8a9c3a.js.map', }, - { name: 'static/js/ArticlePage-31ecd969.ee810b86.js' }, - { name: 'static/js/ArticlePage-31ecd969.ee810b86.js.map' }, + { name: 'static/js/modern.ArticlePage-31ecd969.ee810b86.js' }, + { name: 'static/js/modern.ArticlePage-31ecd969.ee810b86.js.map' }, ], children: {}, childAssets: {}, @@ -66,14 +62,14 @@ describe('pageTypeBundleExtractor', () => { it('should extract bundles used for an ArticlePage', () => { const result = extractBundlesForPageType('ArticlePage'); expect(result).toEqual([ - '../moment-lib-87d47d0c.d5f3ec50.js', - 'commons-0f485567.d710e458.js', - 'commons-7d359b94.ced895c9.js', - 'commons-8493eda2.7bb97fc0.js', - 'commons-92a4fe01.6f43e2d7.js', - 'shared-UddsGWzeoXsaLwaRPMwTQELcfA=-31ecd969.898fb3aa.js', - 'shared-nj6qIml+EtJxDVgSunxJydSAIpY=-253ae210.3f8a9c3a.js', - 'ArticlePage-31ecd969.ee810b86.js', + 'modern.../moment-lib-87d47d0c.d5f3ec50.js', + 'modern.commons-0f485567.d710e458.js', + 'modern.commons-7d359b94.ced895c9.js', + 'modern.commons-8493eda2.7bb97fc0.js', + 'modern.commons-92a4fe01.6f43e2d7.js', + 'modern.shared-UddsGWzeoXsaLwaRPMwTQELcfA=-31ecd969.898fb3aa.js', + 'modern.shared-nj6qIml+EtJxDVgSunxJydSAIpY=-253ae210.3f8a9c3a.js', + 'modern.ArticlePage-31ecd969.ee810b86.js', ]); }); diff --git a/src/client.js b/src/client.js index e0fa6861c7e..c1b3869ba8f 100644 --- a/src/client.js +++ b/src/client.js @@ -10,25 +10,31 @@ import { template, templateStyles } from '#lib/joinUsTemplate'; import loggerNode from '#lib/logger.node'; const logger = loggerNode(); - const data = window.SIMORGH_DATA || {}; const root = document.getElementById('root'); +const isModernBrowser = 'noModule' in document.createElement('script'); +const bundleToExecute = isModernBrowser ? 'modern' : 'legacy'; // Only hydrate the client if we're on the expected path // When on an unknown route, the SSR would be discarded and the user would only // see a blank screen. Avoid this by only hydrating when the embedded page data // and window location agree what the path is. Otherwise, fallback to the SSR. if (window.SIMORGH_DATA.path === window.location.pathname) { - loadableReady(() => { - const cache = createCache({ key: 'bbc' }); + loadableReady( + () => { + const cache = createCache({ key: 'bbc' }); - hydrate( - - - , - root, - ); - }); + hydrate( + + + , + root, + ); + }, + { + namespace: bundleToExecute, // execute the correct __LOADABLE_REQUIRED_CHUNKS__ found in json script tag + }, + ); } else { logger.warn(` Simorgh refused to hydrate. diff --git a/src/poly.js b/src/poly/legacy.js similarity index 100% rename from src/poly.js rename to src/poly/legacy.js diff --git a/src/poly/modern.js b/src/poly/modern.js new file mode 100644 index 00000000000..577e9ab51cf --- /dev/null +++ b/src/poly/modern.js @@ -0,0 +1 @@ +import './safari-nomodule'; // Safari 10.1 supports modules, but does not support the `nomodule` attribute - https://gist.github.com/samthor/64b114e4a4f539915a95b91ffd340acc diff --git a/src/poly/safari-nomodule.js b/src/poly/safari-nomodule.js new file mode 100644 index 00000000000..add2aea7d29 --- /dev/null +++ b/src/poly/safari-nomodule.js @@ -0,0 +1,53 @@ +/* + * Copyright 2017 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Safari 10.1 supports modules, but does not support the `nomodule` attribute - it will + * load + * + * Again: this will **not** prevent inline script, e.g.: + * . + * + * This workaround is possible because Safari supports the non-standard 'beforeload' event. + * This allows us to trap the module and nomodule load. + * + * Note also that `nomodule` is supported in later versions of Safari - it's just 10.1 that + * omits this attribute. + */ +(() => { + const check = document.createElement('script'); + if (!('noModule' in check) && 'onbeforeload' in check) { + let support = false; + document.addEventListener( + 'beforeload', + e => { + if (e.target === check) { + support = true; + } else if (!e.target.hasAttribute('nomodule') || !support) { + return; + } + e.preventDefault(); + }, + true, + ); + + check.type = 'module'; + check.src = '.'; + document.head.appendChild(check); + check.remove(); + } +})(); diff --git a/src/server/Document/__snapshots__/component.test.jsx.snap b/src/server/Document/__snapshots__/component.test.jsx.snap index 5cf526a7339..1d3226536af 100644 --- a/src/server/Document/__snapshots__/component.test.jsx.snap +++ b/src/server/Document/__snapshots__/component.test.jsx.snap @@ -148,13 +148,22 @@ exports[`Document Component should render correctly 1`] = `