4

My website must work with the most basic phone browsers imaginable, because my user base is rural Ethiopian children on very, very basic handsets. (I am using jquery to save handset battery, as most are 'recycled', second/third/fourth-hand and ancient, and I'm being extremely careful about data costs.)

I am trying to set up Babel + Webpack to transpile for the lowest possible supportable target, but I misunderstand the Babel docs, (eg, I started with @babel/preset-env and no targets, as I assumed that not targeting meant maximum compatibility, but this doesn't polyfill), and can't test against my enormous range of target handsets and browsers.

  • Will the below config produce and bundle Javascript that'll run on the maximum possible range of browsers? Is there any way to make it more compatible?

  • I have useBuiltins=usage - will the webpack config below detect repeated imports, and tree shake? If not what do I need to change, or would useBuiltins=entry and require('core-js');require('regenerator-runtime/runtime') in my index.js be better?

  • Using import or require to import bootstrap generates a larger file than the bootstrap distribution, even though I make no reference to it in the JS. How can I get tree-shaking working? Should I not use jquery via npm? EDIT: Tree shaking only happens on PROD builds, and seems to be working with the below configuration.

  • Can I safely use the latest jquery and rely on the polyfilling, rather than 1.12, which has security issues but I'm using as it works on much more browsers?

  • Can I remove @babel/cli, as webpack is running babel? (It works, I just want to be sure I'm getting every ounce of polyfill and compatibility, happy to run babel CLI if better.)

  • Any other missed opportunities/recommendations?

(If relevant, I do not need any chunking - this is a simple app and I am caching indefinitely. I am writing this into a Django static folder, and Django + whitenoise are handling filename fingerprinting and HTTP compression. I will at some point add JS unit tests. I am importing bootstrap JS for polyfills and tree-shaking (although Bootstrap doesn't seem to be shaking), but loading the bootstrap CSS directly from the HTML to avoid cache misses when I update the app.)

packages.json:

{
  ...
  "scripts": {
    "start": "webpack-dev-server --open --mode development",
    "build": "webpack --mode production",
  },
  "devDependencies": {
    "@babel/cli": "^7.10.1",
    "@babel/core": "^7.10.2",
    "@babel/plugin-proposal-class-properties": "^7.10.1",
    "@babel/preset-env": "^7.10.2",
    "axios": "^0.19.2",
    "babel-loader": "^8.1.0",
    "bootstrap": "^4.4.1",
    "jquery": "^1.12.4",  // insecure old version but more compatible
    "popper.js": "^1.16.1",
    "webpack": "^4.43.0",
    "webpack-cli": "^3.3.11",
    "webpack-dev-server": "^3.11.0"
  },
  "dependencies": {
    "@babel/polyfill": "^7.10.1",
    "core-js": "^3.6.5"
  }
}

.babelrc:

{
  "presets": [
    [
      "@babel/env",
      {
        "targets": "cover 100%",
        "useBuiltIns": "usage",
        "corejs": "3.6.5"
      }
    ]
  ],
  "plugins": ["@babel/plugin-proposal-class-properties"]
}

webpack.config.js:

const path = require('path');

module.exports = {
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader"
        }
      }
    ]
  },
  output: {
    filename: 'app.js',
    publicPath: "/static/",
    path: path.join(__dirname, '../djangoproj/app/static/app')
  },
  devServer: {
    writeToDisk: true, // Django serves the content
  }
};

index.js:

import $ from 'jquery';
import bootstrap from 'bootstrap'
import popper from 'popper.js'
import {Controller} from './controller';

$(document).ready(function() {
  const app = new Controller()
})

controller.js:

import {View} from './view';
import {ActivityStore, ElementStore} from './store';
import {Api} from './api';

export class Controller {
  constructor() {
    this.state = {}
    this.api = new Api(config)

// and so on..

Update: I have decided not to progressively polyfill (using <script type="module" ..) as @tfr recommends below, as it is more important to me to test for the lowest phones than optimise newer phones. This is more likely if I'm running the polyfills on my more modern test devices. That said, core-js claims to only polyfill if necessary, so I'm not sure whether nomodules really makes a difference beyond bundle size (so much of my understanding of this technology is choosing which bit of info I trust my understanding of more). I also decided to load Bootstrap and Popper direct from the browser rather than bundled. I am looking into generating a vendor.js but not sure there are any advantages, except perhaps that they'll load before the polyfills in my bundle.

Enormous thanks.

Chris
  • 5,664
  • 6
  • 44
  • 55
  • 1
    Why not just work in strict ES5? This might make some cringe, but es5 is still perfectly valid js & there's not much that es6 does that es5 can't do – admcfajn Jun 09 '20 at 20:39
  • Thanks, that is how I initially started, manually polyfilling, but, despite it being a barebones, fairly simple jquery app, the data is a bit complex and it was immediately a classic old JS code hairball, so I want to use a structured architecture (stores, models, saga-like controller and view), which imports, classes and arrow functions make so much nicer! I am also hoping I can use a more secure version of jquery, and automatic polyfills will be more reliable than research and test. Also I've written ES2016 so long I accidentally use modern syntax and don't catch it on my modern test devices – Chris Jun 09 '20 at 20:50
  • 1
    Boy, I'm glad to be a late adopter... there's so many nuances between var & let & traditional vs arrow-functions, i'm just trying to take bite-size chunks and gradually shift... did a lot of es6 for a vue project back in 2016/17 here's a link to some of my better es5, i find the pattern it uses to be really handy for preventing js-hairballs https://github.com/kanopi/wpqjx/blob/769d47e05db99eb7a6db604ca0471c5ae4b242b5/public/js/wpqjx-public.js#L85 i linked specifically to a jquery when that's being re-purposed as a promise in case that helps. & good luck! – admcfajn Jun 10 '20 at 00:24

1 Answers1

1

Normally the best way would be to bundle dual (modern browser and legacy) the same time, so you don't have to polyfill modern devices. Take a look at this working polyfill example.

Thats how you could load es6 for modern and es5 bundle for legacy browser:

  <!-- main bundle -->
  <script type="module" src="/assets/scripts/main.js"></script>
  <!-- polyfilled legacy browser -->
  <script nomodule src="/assets/scripts/main.legacy.js" defer="defer"></script>

And here the main answer to your question:

Babel Config
============

const legacy = {
  presets: [
    ["@babel/preset-env", {
      targets: {
        browsers: [
          "> 1% in ET", // make sure its the right country code
          "ie >= 6",
          "not dead"
        ]
      },
      useBuiltIns: "usage",
      corejs: 3,
    }]
  ],
  plugins: [
    "@babel/plugin-syntax-dynamic-import",
    "transform-eval"
  ],
  comments: false,
  cacheDirectory: path.join(CACHE_PATH, 'babel-loader')
};


Webpack Config
==============

 const prod = {
  module: {
    rules: [
      {
        enforce: 'pre',
        test: /\.js$/,
        exclude: [
          /node_modules/
        ],
        use: {
          loader: 'eslint-loader',
        },
      },
      {
        test: /\.js$/,
        exclude: [
          /node_modules/
        ],
        loader: "babel-loader",
        options: babelConfig
      }
    ]
  },
  resolve: {
    extensions: ['.js', '.json'],
  }
};

snippets from https://github.com/unic/darvin-webpack-boilerplate/blob/master/webpack/settings/javascript-legacy/index.js
tfrei
  • 181
  • 1
  • 8
  • 1
    I really recommend to use a working dual webpack boilerplate e.g darvin.dev and customize the settings. – tfrei Jun 09 '20 at 22:13
  • `nomodule` a great suggestion thanks. Do I need `@babel/plugin-syntax-dynamic-import` for WebPack 4? What does `transform-eval` help with? (My clients will be that lowest 1% of browsers.) How do I create both builds with a single `npm` command? Huge thanks! – Chris Jun 10 '20 at 13:57
  • 1
    a full featured modern boilerplate with dual building legacy browser will take you weeks without experience. 1) install Darvin 2) setup boilerplate to see demo files 3) try to understand the different builds https://github.com/unic/darvin-webpack-boilerplate/blob/master/.cli/.preview/.scripts/.njk/package.json#L12 you can save yourself this enormous effort. the important thing is that you set the correct setting in the babel config at @babel/preset-env.targets.browser https://babeljs.io/docs/en/6.26.3/babel-plugin-transform-eval see example -> in: es6 arrow function out: es5 func – tfrei Jun 10 '20 at 14:16
  • I don't think that plugin works? https://github.com/babel/babel/issues/7250 removed from core-js https://github.com/babel/babel/pull/7262/files – Chris Jun 12 '20 at 11:04
  • its currently running on various large projects – tfrei Jun 12 '20 at 11:48
  • 1
    I don't doubt it, that issue is the core Babel team saying it can't have worked since 2015, if ever, but it did, because Babel is magic, not science. I am starting to understand why the docs are so unclear and I'm getting no answers - the only person who understands it lives in a cave, casts spells and talks to the spirits... – Chris Jun 12 '20 at 12:31
  • 1
    you're thinking too academically. don't bother with the shape of the screwdriver, tighten the screws. btw your comment applies to the entire NPM ecosystem, the amount of dependencies are a mess – tfrei Jun 13 '20 at 14:58