Unbundling the JavaScript module bundler - Road to Coderful

Unbundling the JavaScript module bundler - Road to Coderful

The landscape of module bundlers has evolved significantly since the days you would manually copy-paste your libraries to create a package for your frontend app. Like many parts of the JS world, the evolution has happened somewhat haphazardly, and the pace of change can feel overwhelming. Has Webpack ever felt like magic to you? How well do you understand what’s really going on under the hood? In this talk, I will uncover the history of JS module bundlers and illustrate how they actually work. Once we have the basics down, I will dive deeper into some of the more advanced topics, such as bundle cache boost and resolving cycling dependencies. At the end of this session, you will have a much more profound understanding of what’s going on behind the scenes.

F3a6662b3cd161c3c2f13604965ed0f2?s=128

Luciano Mammino

August 03, 2020
Tweet

Transcript

  1. Unbundling the JavaScript module bundler "La magia dietro Webpack e

    altri module bundlers" Luciano Mammino - @loige loige.link/bundle-coderful 1
  2. Webpack == PAIN!? twitter.com/search?q=webpack%20pain @loige 2

  3. @loige 3

  4. @loige 4

  5. @loige 5

  6. @loige ❤ 6

  7. @loige 7

  8. @loige 8

  9. @loige 9

  10. It's not Webpack! Module bundling is actually complicated! @loige 10

  11. Hello, I am Luciano! Principal Software Engineer at FabFitFun Blog:

    Twitter: GitHub: loige.co @loige @lmammino nodejsdp.link/3rd 11
  12. loige.link/bundle-coderful 12

  13. 1. Why we need modules 2. JavaScript module systems 3.

    How a module bundler works 4. Webpack in 2 minutes! 5. Advanced module bundling Agenda @loige 13
  14. https://poo.loige.co @loige 14

  15. App features @loige 15

  16. Dynamic DOM manipulation React, Angular, Vue are so... overrated! @loige

    16
  17. Dynamic Favicon favico.js @loige 17

  18. Custom animated tooltips tippy.js @loige 18

  19. Confetti rainfall dom-confetti @loige 19

  20. Persist state through Local Storage + UUIDs + store2 uuidjs

    @loige 20
  21. @loige 21

  22. @loige 21

  23. 7 Requests only for the JS code! @loige 21

  24. Current scenario zepto@1.2.0/dist/zepto.min.js uuidjs@4.0.3/dist/uuid.core.js store2@2.7.0/dist/store2.min.js tippy.js@2.2.3/dist/tippy.all.min.js confetti@0.0.10/lib/main.js favico.js@0.3.10/favico-0.3.10.min.js @loige 22

  25. Ideal scenario zepto@1.2.0/dist/zepto.min.js uuidjs@4.0.3/dist/uuid.core.js store2@2.7.0/dist/store2.min.js tippy.js@2.2.3/dist/tippy.all.min.js confetti@0.0.10/lib/main.js favico.js@0.3.10/favico-0.3.10.min.js vendors.js +

    + + + + = @loige 23
  26. How to do this? @loige 24

  27. ./buildVendors.sh > vendors.js buildVendors.sh $ @loige 25

  28. npx lumpy build $ github.com/lmammino/lumpy @loige 26 . 1

  29. # lumpy.txt https://unpkg.com/zepto@1.2.0/dist/zepto.min.js https://unpkg.com/uuidjs@4.0.3/dist/uuid.core.js https://unpkg.com/store2@2.7.0/dist/store2.min.js https://unpkg.com/tippy.js@2.2.3/dist/tippy.all.min.js https://unpkg.com/confetti-js@0.0.11/dist/index.min.js https://unpkg.com/dom-confetti@0.0.10/lib/main.js https://unpkg.com/favico.js@0.3.10/favico-0.3.10.min.js Lumpy

    allows you to define all the vendors in a text file @loige 26 . 2
  30. lumpy build $ 1. Downloads the files from lumpy.txt (and

    caches them) 2. Concatenates the content of the files 3. Minifies the resulting source code (using ) 4. Saves the resulting content in vendors.js babel-minify @loige 26 . 3
  31. 7 requests 2 requests @loige 27

  32. 7 requests 2 requests @loige Even better if you "minify"

    these! 27
  33. Concatenation + Minification @loige 28

  34. This is good... @loige 29

  35. This is good... @loige 29

  36. This is good... ... in 2008 was @loige 29

  37. Today... We can do better! @loige 30

  38. Updating them should be easy We shouldn't worry about transitive

    dependencies (dependencies of dependencies) Order of imports shouldn't really matter We rely on dependencies! @loige 31
  39. Dependency (or coupling) a state in which one object uses

    a function of another object — Wikipedia @loige 32
  40. Reusable dependencies... Modules! @loige 33

  41. Modules The bricks for structuring non-trivial applications, but also the

    main mechanism to enforce information hiding by keeping private all the functions and variables that are not explicitly marked to be exported — * Node.js Design Patterns * yeah, I quite like quoting my stuff... @loige 34
  42. 1. Why we need modules 2. JavaScript module systems 3.

    How a module bundler works 4. Webpack in 2 minutes! 5. Advanced module bundling Agenda @loige 35
  43. Meet my friend I.I.F.E. (Immediately Invoked Function Expression) loige.link/iife @loige

    36
  44. We generally define a function this way @loige 37

  45. We generally define a function this way const sum =

    (a, b) => a + b @loige 37
  46. We generally define a function this way const sum =

    (a, b) => a + b or @loige 37
  47. We generally define a function this way const sum =

    (a, b) => a + b function sum(a, b) { return a + b } or @loige 37
  48. We generally define a function this way const sum =

    (a, b) => a + b function sum(a, b) { return a + b } or then, at some point, we execute it... @loige 37
  49. We generally define a function this way const sum =

    (a, b) => a + b function sum(a, b) { return a + b } or then, at some point, we execute it... const four = sum(2, 2) @loige 37
  50. A function in JS creates an isolated scope @loige 38

  51. A function in JS creates an isolated scope (a, b)

    => { const secretString = "Hello" return a + b } console.log(secretString) // undefined @loige 38
  52. A function in JS creates an isolated scope (a, b)

    => { const secretString = "Hello" return a + b } console.log(secretString) // undefined secretString is not visible outside the function @loige 38
  53. IIFE allows you to define an isolated scope that executes

    itself (arg1, arg2) => { // do stuff here const iAmNotVisibleOutside = true } @loige 39
  54. IIFE allows you to define an isolated scope that executes

    itself (arg1, arg2) => { // do stuff here const iAmNotVisibleOutside = true } A function with its own scope @loige 39
  55. )(someArg1, someArg2) IIFE allows you to define an isolated scope

    that executes itself (arg1, arg2) => { // do stuff here const iAmNotVisibleOutside = true } ( @loige 39
  56. )(someArg1, someArg2) IIFE allows you to define an isolated scope

    that executes itself (arg1, arg2) => { // do stuff here const iAmNotVisibleOutside = true } ( This wrapper executes the function immediately and passes arguments from the outer scope @loige 39
  57. IIFE is a recurring pattern in JavaScript modules @loige 40

  58. Let's implement a module that provides: Information hiding exported functionalities

    @loige 41
  59. const myModule = (() => { const privateFoo = ()

    => { /* ... */ } const privateBar = [ /* ... */ ] const exported = { publicFoo: () => { /* ... */ }, publicBar: [ /* ... */ ] }; return exported })() myModule.publicFoo() myModule.publicBar[0] myModule.privateFoo // undefined myModule.privateBar // undefined privateFoo // undefined privateBar // undefined @loige 42
  60. const myModule = (() => { const privateFoo = ()

    => { /* ... */ } const privateBar = [ /* ... */ ] const exported = { publicFoo: () => { /* ... */ }, publicBar: [ /* ... */ ] }; return exported })() A module myModule.publicFoo() myModule.publicBar[0] myModule.privateFoo // undefined myModule.privateBar // undefined privateFoo // undefined privateBar // undefined @loige 42
  61. const myModule = (() => { const privateFoo = ()

    => { /* ... */ } const privateBar = [ /* ... */ ] const exported = { publicFoo: () => { /* ... */ }, publicBar: [ /* ... */ ] }; return exported })() A module myModule.publicFoo() myModule.publicBar[0] myModule.privateFoo // undefined myModule.privateBar // undefined privateFoo // undefined privateBar // undefined IIFE Creates an isolated scope and executes it @loige 42
  62. const myModule = (() => { const privateFoo = ()

    => { /* ... */ } const privateBar = [ /* ... */ ] const exported = { publicFoo: () => { /* ... */ }, publicBar: [ /* ... */ ] }; return exported })() A module myModule.publicFoo() myModule.publicBar[0] myModule.privateFoo // undefined myModule.privateBar // undefined privateFoo // undefined privateBar // undefined information hiding non-exported functionality @loige 42
  63. const myModule = (() => { const privateFoo = ()

    => { /* ... */ } const privateBar = [ /* ... */ ] const exported = { publicFoo: () => { /* ... */ }, publicBar: [ /* ... */ ] }; return exported })() A module myModule.publicFoo() myModule.publicBar[0] myModule.privateFoo // undefined myModule.privateBar // undefined privateFoo // undefined privateBar // undefined defines exported functionalities @loige 42
  64. const myModule = (() => { const privateFoo = ()

    => { /* ... */ } const privateBar = [ /* ... */ ] const exported = { publicFoo: () => { /* ... */ }, publicBar: [ /* ... */ ] }; return exported })() A module myModule.publicFoo() myModule.publicBar[0] myModule.privateFoo // undefined myModule.privateBar // undefined privateFoo // undefined privateBar // undefined propagates the exports to the outer scope (assigning it to myModule) @loige 42
  65. const myModule = (() => { const privateFoo = ()

    => { /* ... */ } const privateBar = [ /* ... */ ] const exported = { publicFoo: () => { /* ... */ }, publicBar: [ /* ... */ ] }; return exported })() A module myModule.publicFoo() myModule.publicBar[0] myModule.privateFoo // undefined myModule.privateBar // undefined privateFoo // undefined privateBar // undefined Can access exported functionalities @loige 42
  66. const myModule = (() => { const privateFoo = ()

    => { /* ... */ } const privateBar = [ /* ... */ ] const exported = { publicFoo: () => { /* ... */ }, publicBar: [ /* ... */ ] }; return exported })() A module myModule.publicFoo() myModule.publicBar[0] myModule.privateFoo // undefined myModule.privateBar // undefined privateFoo // undefined privateBar // undefined No visibility for the non-exported ones @loige 42
  67. We want modules to be reusable across different apps and

    organisations... ...we need A STANDARD MODULE format! @loige 43
  68. Module system features Must have Simple syntax for import /

    export Information hiding Allows to define modules in separate files Modules can import from other modules (nested dependencies) @loige 44 . 1
  69. Module system features Nice to have Ability to import module

    subsets Avoid naming collision Asynchronous module loading Seamless support for Browsers & Server-side @loige 44 . 2
  70. JavaScript module systems globals CommonJS (Node.js) AMD (Require.js / Dojo)

    UMD ES2015 Modules (ESM) Many others (SystemJS, ...) @loige 45
  71. Globals var $, jQuery $ = jQuery = (() =>

    { return { /* ... */ } })() // ... use $ or jQuery in the global scope $.find('.button').remove() @loige 46
  72. Globals Might generate naming collisions (e.g. $ overrides browser global

    variable) Modules needs to be "fully loaded" in the right order Cannot import parts of modules @loige 47
  73. // or import single functionality const { concat } =

    require('./loDash') concat([1], [2], [3]) // app.js // import full module const _ = require('./loDash') _.concat([1], [2], [3]) // loDash.js const loDash = { /* ... */ } module.exports = loDash CommonJS @loige 48
  74. // or import single functionality const { concat } =

    require('./loDash') concat([1], [2], [3]) // app.js // import full module const _ = require('./loDash') _.concat([1], [2], [3]) // loDash.js const loDash = { /* ... */ } module.exports = loDash CommonJS module @loige 48
  75. // or import single functionality const { concat } =

    require('./loDash') concat([1], [2], [3]) // app.js // import full module const _ = require('./loDash') _.concat([1], [2], [3]) // loDash.js const loDash = { /* ... */ } module.exports = loDash CommonJS module app using module @loige 48
  76. // or import single functionality const { concat } =

    require('./loDash') concat([1], [2], [3]) // app.js // import full module const _ = require('./loDash') _.concat([1], [2], [3]) // loDash.js const loDash = { /* ... */ } module.exports = loDash CommonJS module app using module @loige 48
  77. // or import single functionality const { concat } =

    require('./loDash') concat([1], [2], [3]) // app.js // import full module const _ = require('./loDash') _.concat([1], [2], [3]) // loDash.js const loDash = { /* ... */ } module.exports = loDash CommonJS module app using module @loige 48
  78. CommonJS No naming collisions (imported modules can be renamed) Huge

    repository of modules through Synchronous import only Works natively on the server side only (Node.js) NPM @loige 49
  79. AMD ( ) Asynchronous Module Definition Require.js // jquery-1.9.0.js define(

    'jquery', ['sizzle', 'jqueryUI'], function (sizzle, jqueryUI) { // Returns the exported value return function () { // ... } } ) @loige 50
  80. AMD ( ) Asynchronous Module Definition Require.js // jquery-1.9.0.js define(

    'jquery', ['sizzle', 'jqueryUI'], function (sizzle, jqueryUI) { // Returns the exported value return function () { // ... } } ) module @loige 50
  81. AMD ( ) Asynchronous Module Definition Require.js // jquery-1.9.0.js define(

    'jquery', ['sizzle', 'jqueryUI'], function (sizzle, jqueryUI) { // Returns the exported value return function () { // ... } } ) module module name @loige 50
  82. AMD ( ) Asynchronous Module Definition Require.js // jquery-1.9.0.js define(

    'jquery', ['sizzle', 'jqueryUI'], function (sizzle, jqueryUI) { // Returns the exported value return function () { // ... } } ) module dependencies @loige 50
  83. AMD ( ) Asynchronous Module Definition Require.js // jquery-1.9.0.js define(

    'jquery', ['sizzle', 'jqueryUI'], function (sizzle, jqueryUI) { // Returns the exported value return function () { // ... } } ) module factory function used to construct the module, receives the dependencies as arguments @loige 50
  84. AMD ( ) Asynchronous Module Definition Require.js // jquery-1.9.0.js define(

    'jquery', ['sizzle', 'jqueryUI'], function (sizzle, jqueryUI) { // Returns the exported value return function () { // ... } } ) module exported value @loige 50
  85. AMD ( ) Asynchronous Module Definition Require.js // app.js //

    define paths requirejs.config({ baseUrl: 'js/lib', paths: { jquery: 'jquery-1.9.0' } }) define(['jquery'], function ($) { // this is executed only when jquery // and its deps are loaded }); @loige 51
  86. AMD ( ) Asynchronous Module Definition Require.js // app.js //

    define paths requirejs.config({ baseUrl: 'js/lib', paths: { jquery: 'jquery-1.9.0' } }) define(['jquery'], function ($) { // this is executed only when jquery // and its deps are loaded }); app @loige 51
  87. AMD ( ) Asynchronous Module Definition Require.js // app.js //

    define paths requirejs.config({ baseUrl: 'js/lib', paths: { jquery: 'jquery-1.9.0' } }) define(['jquery'], function ($) { // this is executed only when jquery // and its deps are loaded }); app Require.js config jquery will be loaded from ://<currentDomain>/js/lib/jquery-1.9.0.js @loige 51
  88. AMD ( ) Asynchronous Module Definition Require.js // app.js //

    define paths requirejs.config({ baseUrl: 'js/lib', paths: { jquery: 'jquery-1.9.0' } }) define(['jquery'], function ($) { // this is executed only when jquery // and its deps are loaded }); app app main function Has jquery as dependency @loige 51
  89. AMD ( ) Asynchronous Module Definition Require.js Asynchronous modules Works

    on Browsers and Server side Very verbose and convoluted syntax (my opinion™) @loige 52
  90. UMD Universal Module Definition A module definition that is compatible

    with Global modules, CommonJS & AMD github.com/umdjs/umd @loige 53 . 1
  91. (function (root, factory) { if (typeof exports === 'object') {

    // CommonJS module.exports = factory(require('dep')) } else if (typeof define === 'function' && define.amd) { // AMD define(['dep'], function (dep) { return (root.returnExportsGlobal = factory(dep)) }) } else { // Global Variables root.myModule = factory(root.dep) } }(this, function (dep) { // Your actual module return {} })) loige.link/umd @loige 53 . 2
  92. (function (root, factory) { if (typeof exports === 'object') {

    // CommonJS module.exports = factory(require('dep')) } else if (typeof define === 'function' && define.amd) { // AMD define(['dep'], function (dep) { return (root.returnExportsGlobal = factory(dep)) }) } else { // Global Variables root.myModule = factory(root.dep) } }(this, function (dep) { // Your actual module return {} })) loige.link/umd IIFE with arguments: - Current scope (this) and the module factory function. - "dep" is a sample dependency of the module. @loige 53 . 2
  93. (function (root, factory) { if (typeof exports === 'object') {

    // CommonJS module.exports = factory(require('dep')) } else if (typeof define === 'function' && define.amd) { // AMD define(['dep'], function (dep) { return (root.returnExportsGlobal = factory(dep)) }) } else { // Global Variables root.myModule = factory(root.dep) } }(this, function (dep) { // Your actual module return {} })) loige.link/umd @loige 53 . 2
  94. (function (root, factory) { if (typeof exports === 'object') {

    // CommonJS module.exports = factory(require('dep')) } else if (typeof define === 'function' && define.amd) { // AMD define(['dep'], function (dep) { return (root.returnExportsGlobal = factory(dep)) }) } else { // Global Variables root.myModule = factory(root.dep) } }(this, function (dep) { // Your actual module return {} })) loige.link/umd @loige 53 . 2
  95. (function (root, factory) { if (typeof exports === 'object') {

    // CommonJS module.exports = factory(require('dep')) } else if (typeof define === 'function' && define.amd) { // AMD define(['dep'], function (dep) { return (root.returnExportsGlobal = factory(dep)) }) } else { // Global Variables root.myModule = factory(root.dep) } }(this, function (dep) { // Your actual module return {} })) loige.link/umd @loige 53 . 2
  96. Allows you to define modules that can be used by

    almost any module loader Complex, the wrapper code is almost impossible to write manually UMD Universal Module Definition @loige 53 . 3
  97. ES2015 modules Cool & broad subject, it would deserve it's

    own talk Wanna know more? / syntax reference import export ECMAScript modules in browsers ES modules: A cartoon deep-dive ES Modules in Node Today! @loige 54 . 1
  98. // calculator.js const add = (num1, num2) => num1 +

    num2 const sub = (num1, num2) => num1 - num2 const div = (num1, num2) => num1 / num2 const mul = (num1, num2) => num1 * num2 export { add, sub, div, mul } // app.js import { add } from './calculator' console.log(add(2,2)) // 4 ES2015 modules @loige 54 . 2
  99. // calculator.js const add = (num1, num2) => num1 +

    num2 const sub = (num1, num2) => num1 - num2 const div = (num1, num2) => num1 / num2 const mul = (num1, num2) => num1 * num2 export { add, sub, div, mul } // app.js import { add } from './calculator' console.log(add(2,2)) // 4 ES2015 modules module @loige 54 . 2
  100. // calculator.js const add = (num1, num2) => num1 +

    num2 const sub = (num1, num2) => num1 - num2 const div = (num1, num2) => num1 / num2 const mul = (num1, num2) => num1 * num2 export { add, sub, div, mul } // app.js import { add } from './calculator' console.log(add(2,2)) // 4 ES2015 modules module exported functionalities @loige 54 . 2
  101. // calculator.js const add = (num1, num2) => num1 +

    num2 const sub = (num1, num2) => num1 - num2 const div = (num1, num2) => num1 / num2 const mul = (num1, num2) => num1 * num2 export { add, sub, div, mul } // app.js import { add } from './calculator' console.log(add(2,2)) // 4 ES2015 modules module app @loige 54 . 2
  102. // calculator.js const add = (num1, num2) => num1 +

    num2 const sub = (num1, num2) => num1 - num2 const div = (num1, num2) => num1 / num2 const mul = (num1, num2) => num1 * num2 export { add, sub, div, mul } // app.js import { add } from './calculator' console.log(add(2,2)) // 4 ES2015 modules module app import specific functionality @loige 54 . 2
  103. // index.html <html> <body> <!-- ... --> <script type="module"> import

    { add } from 'calculator.js' console.log(add(2,2)) // 4 </script> </body> </html> ES2015 modules @loige 54 . 3
  104. // index.html <html> <body> <!-- ... --> <script type="module"> import

    { add } from 'calculator.js' console.log(add(2,2)) // 4 </script> </body> </html> ES2015 modules "works" in some modern browsers @loige 54 . 3
  105. ES2015 modules Syntactically very similar to CommonJS... BUT import &

    export are static (allow static analysis of dependencies) It is a (still work in progress) standard format Works (almost) seamlessly in browsers & servers @loige 54 . 4
  106. So many options... Current most used practice: Use CommonJS or

    ES2015 & create "compiled bundles" @loige 55
  107. 1. Why we need modules 2. JavaScript module systems 3.

    How a module bundler works 4. Webpack in 2 minutes! 5. Advanced module bundling Agenda @loige 56
  108. Let's try to use CommonJS in the browser @loige 57

  109. const $ = require('zepto') const tippy = require('tippy.js') const UUID

    = require('uuidjs') const { confetti } = require('dom-confetti/src/main') const store = require('store2') const Favico = require('favico.js') !(function () { const colors = ['#a864fd', '#29cdff', '#78ff44', '#ff718d', '#fdff6a'] const todoApp = (rootEl, opt = {}) => { const todos = opt.todos || [] let completedTasks = opt.completedTasks || 0 const onChange = opt.onChange || (() => {}) const list = rootEl.find('.todo-list') const footer = rootEl.find('.footer') const todoCount = footer.find('.todo-count') const insertInput = rootEl.find('.add-todo-box input') const insertBtn = rootEl.find('.add-todo-box button') const render = () => { let tips list.html('') The browser doesn't know how to process require. It doesn't support CommonJS! @loige 58
  110. Module Bundler A tool that takes modules with dependencies and

    emits static assets representing those modules Those static assets can be processed by browsers! @loige 59
  111. Dependency graph A graph built by connecting every module with

    its direct dependencies. app dependency A dependency B dependency A2 shared dependency @loige 60
  112. A module bundler has to: 1. Construct the dependency graph

    (Dependency Resolution) 2. Assemble the modules in the graph into a single executable asset (Packing) @loige 61
  113. // app.js const calculator = require('./calculator') const log = require('./log')

    log(calculator('2 + 2 / 4')) // log.js module.exports = console.log // calculator.js const parser = require('./parser') const resolver = require('./resolver') module.exports = (expr) => resolver(parser(expr)) // parser.js module.exports = (expr) => { /* ... */ } // resolver.js module.exports = (tokens) => { /* ... */ } Dependency resolution 62 @loige
  114. // app.js const calculator = require('./calculator') const log = require('./log')

    log(calculator('2 + 2 / 4')) // log.js module.exports = console.log // calculator.js const parser = require('./parser') const resolver = require('./resolver') module.exports = (expr) => resolver(parser(expr)) // parser.js module.exports = (expr) => { /* ... */ } // resolver.js module.exports = (tokens) => { /* ... */ } Dependency resolution app 62 (entrypoint) (1) @loige
  115. // app.js const calculator = require('./calculator') const log = require('./log')

    log(calculator('2 + 2 / 4')) // log.js module.exports = console.log // calculator.js const parser = require('./parser') const resolver = require('./resolver') module.exports = (expr) => resolver(parser(expr)) // parser.js module.exports = (expr) => { /* ... */ } // resolver.js module.exports = (tokens) => { /* ... */ } Dependency resolution app calculator 62 (entrypoint) (1) @loige
  116. // app.js const calculator = require('./calculator') const log = require('./log')

    log(calculator('2 + 2 / 4')) // log.js module.exports = console.log // calculator.js const parser = require('./parser') const resolver = require('./resolver') module.exports = (expr) => resolver(parser(expr)) // parser.js module.exports = (expr) => { /* ... */ } // resolver.js module.exports = (tokens) => { /* ... */ } Dependency resolution app calculator 62 (entrypoint) (1) (2) @loige
  117. // app.js const calculator = require('./calculator') const log = require('./log')

    log(calculator('2 + 2 / 4')) // log.js module.exports = console.log // calculator.js const parser = require('./parser') const resolver = require('./resolver') module.exports = (expr) => resolver(parser(expr)) // parser.js module.exports = (expr) => { /* ... */ } // resolver.js module.exports = (tokens) => { /* ... */ } Dependency resolution app calculator parser 62 (entrypoint) (1) (2) @loige
  118. // app.js const calculator = require('./calculator') const log = require('./log')

    log(calculator('2 + 2 / 4')) // log.js module.exports = console.log // calculator.js const parser = require('./parser') const resolver = require('./resolver') module.exports = (expr) => resolver(parser(expr)) // parser.js module.exports = (expr) => { /* ... */ } // resolver.js module.exports = (tokens) => { /* ... */ } Dependency resolution app calculator parser 62 (entrypoint) (1) (2) (3) @loige
  119. // app.js const calculator = require('./calculator') const log = require('./log')

    log(calculator('2 + 2 / 4')) // log.js module.exports = console.log // calculator.js const parser = require('./parser') const resolver = require('./resolver') module.exports = (expr) => resolver(parser(expr)) // parser.js module.exports = (expr) => { /* ... */ } // resolver.js module.exports = (tokens) => { /* ... */ } Dependency resolution app calculator parser resolver 62 (entrypoint) (1) (2) (3) @loige
  120. // app.js const calculator = require('./calculator') const log = require('./log')

    log(calculator('2 + 2 / 4')) // log.js module.exports = console.log // calculator.js const parser = require('./parser') const resolver = require('./resolver') module.exports = (expr) => resolver(parser(expr)) // parser.js module.exports = (expr) => { /* ... */ } // resolver.js module.exports = (tokens) => { /* ... */ } Dependency resolution app calculator parser resolver 62 (entrypoint) (1) (2) (3) (4) @loige
  121. // app.js const calculator = require('./calculator') const log = require('./log')

    log(calculator('2 + 2 / 4')) // log.js module.exports = console.log // calculator.js const parser = require('./parser') const resolver = require('./resolver') module.exports = (expr) => resolver(parser(expr)) // parser.js module.exports = (expr) => { /* ... */ } // resolver.js module.exports = (tokens) => { /* ... */ } Dependency resolution app calculator log parser resolver 62 (entrypoint) (1) (2) (3) (4) @loige
  122. // app.js const calculator = require('./calculator') const log = require('./log')

    log(calculator('2 + 2 / 4')) // log.js module.exports = console.log // calculator.js const parser = require('./parser') const resolver = require('./resolver') module.exports = (expr) => resolver(parser(expr)) // parser.js module.exports = (expr) => { /* ... */ } // resolver.js module.exports = (tokens) => { /* ... */ } Dependency resolution app calculator log parser resolver 62 (entrypoint) (1) (2) (3) (4) (5) @loige
  123. During dependency resolution, the bundler creates a modules map @loige

    63
  124. During dependency resolution, the bundler creates a modules map {

    } @loige 63
  125. During dependency resolution, the bundler creates a modules map {

    } './app': (module, require) => { … }, @loige 63
  126. During dependency resolution, the bundler creates a modules map {

    } './app': (module, require) => { … }, './calculator': (module, require) => { … }, @loige 63
  127. During dependency resolution, the bundler creates a modules map {

    } './app': (module, require) => { … }, './calculator': (module, require) => { … }, './parser': (module, require) => { … }, @loige 63
  128. During dependency resolution, the bundler creates a modules map {

    } './app': (module, require) => { … }, './calculator': (module, require) => { … }, './parser': (module, require) => { … }, './resolver': (module, require) => { … }, @loige 63
  129. During dependency resolution, the bundler creates a modules map {

    } './app': (module, require) => { … }, './calculator': (module, require) => { … }, './log': (module, require) => { … }, './parser': (module, require) => { … }, './resolver': (module, require) => { … }, @loige 63
  130. During dependency resolution, the bundler creates a modules map {

    } './app': (module, require) => { … }, './calculator': (module, require) => { … }, './log': (module, require) => { … }, './parser': (module, require) => { … }, './resolver': (module, require) => { … }, require path @loige 63
  131. During dependency resolution, the bundler creates a modules map {

    } './app': (module, require) => { … }, './calculator': (module, require) => { … }, './log': (module, require) => { … }, './parser': (module, require) => { … }, './resolver': (module, require) => { … }, module factory function @loige 63
  132. During dependency resolution, the bundler creates a modules map {

    } './app': (module, require) => { … }, './calculator': (module, require) => { … }, './log': (module, require) => { … }, './parser': (module, require) => { … }, './resolver': (module, require) => { … }, const parser = require('./parser');const resolver = require('./resolver');module.exports = (expr) => resolver(parser(expr)) @loige 63
  133. During dependency resolution, the bundler creates a modules map {

    } './app': (module, require) => { … }, './calculator': (module, require) => { … }, './log': (module, require) => { … }, './parser': (module, require) => { … }, './resolver': (module, require) => { … }, const parser = require('./parser');const resolver = require('./resolver');module.exports = (expr) => resolver(parser(expr)) @loige 63
  134. During dependency resolution, the bundler creates a modules map {

    } './app': (module, require) => { … }, './calculator': (module, require) => { … }, './log': (module, require) => { … }, './parser': (module, require) => { … }, './resolver': (module, require) => { … }, const parser = require('./parser');const resolver = require('./resolver');module.exports = (expr) => resolver(parser(expr)) @loige 63
  135. Packing { --- : --- --- : ---- --- :

    -- } Modules map @loige 64
  136. Packing { --- : --- --- : ---- --- :

    -- } Modules map @loige 64
  137. Packing .js { --- : --- --- : ---- ---

    : -- } Modules map Single executable JS file @loige 64
  138. Packed executable file ((modulesMap) => { const require = (name)

    => { const module = { exports: {} } modulesMap[name](module, require) return module.exports } require('./app') })( { './app': (module, require) => { … }, './calculator': (module, require) => { … }, './log': (module, require) => { … }, './parser': (module, require) => { … }, './resolver': (module, require) => { … } } ) 65 @loige
  139. Packed executable file ((modulesMap) => { const require = (name)

    => { const module = { exports: {} } modulesMap[name](module, require) return module.exports } require('./app') })( { './app': (module, require) => { … }, './calculator': (module, require) => { … }, './log': (module, require) => { … }, './parser': (module, require) => { … }, './resolver': (module, require) => { … } } ) IIFE passing the modules map as argument 65 @loige
  140. Packed executable file ((modulesMap) => { const require = (name)

    => { const module = { exports: {} } modulesMap[name](module, require) return module.exports } require('./app') })( { './app': (module, require) => { … }, './calculator': (module, require) => { … }, './log': (module, require) => { … }, './parser': (module, require) => { … }, './resolver': (module, require) => { … } } ) Custom require function: it will load the modules by evaluating the code from the modules map 65 @loige
  141. Packed executable file ((modulesMap) => { const require = (name)

    => { const module = { exports: {} } modulesMap[name](module, require) return module.exports } require('./app') })( { './app': (module, require) => { … }, './calculator': (module, require) => { … }, './log': (module, require) => { … }, './parser': (module, require) => { … }, './resolver': (module, require) => { … } } ) A reference to a module with an empty module.exports. This will be filled at evaluation time 65 @loige
  142. Packed executable file ((modulesMap) => { const require = (name)

    => { const module = { exports: {} } modulesMap[name](module, require) return module.exports } require('./app') })( { './app': (module, require) => { … }, './calculator': (module, require) => { … }, './log': (module, require) => { … }, './parser': (module, require) => { … }, './resolver': (module, require) => { … } } ) Invoking the factory function for the given module name. (Service locator pattern) 65 @loige
  143. Packed executable file ((modulesMap) => { const require = (name)

    => { const module = { exports: {} } modulesMap[name](module, require) return module.exports } require('./app') })( { './app': (module, require) => { … }, './calculator': (module, require) => { … }, './log': (module, require) => { … }, './parser': (module, require) => { … }, './resolver': (module, require) => { … } } ) The current reference module is passed, the factory function will modify this object by adding the proper exported values. 65 @loige
  144. Packed executable file ((modulesMap) => { const require = (name)

    => { const module = { exports: {} } modulesMap[name](module, require) return module.exports } require('./app') })( { './app': (module, require) => { … }, './calculator': (module, require) => { … }, './log': (module, require) => { … }, './parser': (module, require) => { … }, './resolver': (module, require) => { … } } ) The custom require function is passed so, modules can recursively require other modules 65 @loige
  145. Packed executable file ((modulesMap) => { const require = (name)

    => { const module = { exports: {} } modulesMap[name](module, require) return module.exports } require('./app') })( { './app': (module, require) => { … }, './calculator': (module, require) => { … }, './log': (module, require) => { … }, './parser': (module, require) => { … }, './resolver': (module, require) => { … } } ) The resulting module.exports is returned 65 @loige
  146. Packed executable file ((modulesMap) => { const require = (name)

    => { const module = { exports: {} } modulesMap[name](module, require) return module.exports } require('./app') })( { './app': (module, require) => { … }, './calculator': (module, require) => { … }, './log': (module, require) => { … }, './parser': (module, require) => { … }, './resolver': (module, require) => { … } } ) The entrypoint module is required, triggering the actual execution of the business logic 65 @loige
  147. Packed executable file ((modulesMap) => { const require = (name)

    => { const module = { exports: {} } modulesMap[name](module, require) return module.exports } require('./app') })( { './app': (module, require) => { … }, './calculator': (module, require) => { … }, './log': (module, require) => { … }, './parser': (module, require) => { … }, './resolver': (module, require) => { … } } ) 65 @loige
  148. Now you know how Module bundlers work! And how to

    convert code written using CommonJS to a single file that works in the browser @loige 66
  149. A challenge for you! If you do, ... I'll arrange

    a prize for you! TIP: you can use or to parse JavaScript files (look for require and module.exports) and to map relative module paths to actual files in the filesystem. Need an inspiration? Check the awesome and ! let me know acorn babel-parser resolve minipack @adamisnotdead's w_bp_ck Can you build a (simple) module bundler from scratch? @loige 67
  150. 1. Why we need modules 2. JavaScript module systems 3.

    How a module bundler works 4. Webpack in 2 minutes! 5. Advanced module bundling Agenda @loige 68
  151. A state of the art module bundler for the web

    @loige 69
  152. npm install webpack webpack-cli @loige 70

  153. webpack app.js yep, recent versions of Webpack work without config!

    @loige 71
  154. webpack --mode=development app.js Do not compress the code and add

    annotations! @loige 72
  155. loige.link/sample-webpacked-file @loige 73

  156. Webpack concepts Entry point: the starting file for dependency resolution.

    Output: the destination file (bundled file). Loaders: algorithms to parse different file types and convert them into executable javascript (e.g. babel, typescript, but also CSS, images or other static assets) Plugins: do extra things (e.g. generate a wrapping HTML or analysis tools) @loige 74
  157. const { resolve, join } = require('path') const CompressionPlugin =

    require('compression-webpack-plugin') module.exports = { entry: './app.js', output: { path: resolve(join(__dirname, 'build')), filename: 'app.js' }, module: { rules: [ { test: /\.js$/, exclude: /(node_modules|bower_components)/, use: { loader: 'babel-loader', options: { presets: [ ['@babel/preset-env'] ] } } } ] }, plugins: [ new CompressionPlugin() ] } Webpack.config.js @loige 75
  158. const { resolve, join } = require('path') const CompressionPlugin =

    require('compression-webpack-plugin') module.exports = { entry: './app.js', output: { path: resolve(join(__dirname, 'build')), filename: 'app.js' }, module: { rules: [ { test: /\.js$/, exclude: /(node_modules|bower_components)/, use: { loader: 'babel-loader', options: { presets: [ ['@babel/preset-env'] ] } } } ] }, plugins: [ new CompressionPlugin() ] } Webpack.config.js Entrypoint Build the dependency graph starting from ./app.js @loige 75
  159. const { resolve, join } = require('path') const CompressionPlugin =

    require('compression-webpack-plugin') module.exports = { entry: './app.js', output: { path: resolve(join(__dirname, 'build')), filename: 'app.js' }, module: { rules: [ { test: /\.js$/, exclude: /(node_modules|bower_components)/, use: { loader: 'babel-loader', options: { presets: [ ['@babel/preset-env'] ] } } } ] }, plugins: [ new CompressionPlugin() ] } Webpack.config.js Output Save the resulting bundled file in ./build/app.js @loige 75
  160. const { resolve, join } = require('path') const CompressionPlugin =

    require('compression-webpack-plugin') module.exports = { entry: './app.js', output: { path: resolve(join(__dirname, 'build')), filename: 'app.js' }, module: { rules: [ { test: /\.js$/, exclude: /(node_modules|bower_components)/, use: { loader: 'babel-loader', options: { presets: [ ['@babel/preset-env'] ] } } } ] }, plugins: [ new CompressionPlugin() ] } Webpack.config.js Loaders All the files matching "*.js" are processed with babel and converted to ES5 Javascript @loige 75
  161. const { resolve, join } = require('path') const CompressionPlugin =

    require('compression-webpack-plugin') module.exports = { entry: './app.js', output: { path: resolve(join(__dirname, 'build')), filename: 'app.js' }, module: { rules: [ { test: /\.js$/, exclude: /(node_modules|bower_components)/, use: { loader: 'babel-loader', options: { presets: [ ['@babel/preset-env'] ] } } } ] }, plugins: [ new CompressionPlugin() ] } Webpack.config.js Plugins Uses a plugin that generates a gzipped copy of every emitted file. @loige 75
  162. Everything is a module import React, { Component } from

    'react' import logo from './logo.svg' import './App.css' class App extends Component { render() { return ( <div className="App"> <header className="App-header"> <img src={logo} className="App-logo" alt="logo" /> <h1 className="App-title">Welcome to React</h1> </header> </div> ); } } export default App 76
  163. Everything is a module import React, { Component } from

    'react' import logo from './logo.svg' import './App.css' class App extends Component { render() { return ( <div className="App"> <header className="App-header"> <img src={logo} className="App-logo" alt="logo" /> <h1 className="App-title">Welcome to React</h1> </header> </div> ); } } export default App 76
  164. Everything is a module import React, { Component } from

    'react' import logo from './logo.svg' import './App.css' class App extends Component { render() { return ( <div className="App"> <header className="App-header"> <img src={logo} className="App-logo" alt="logo" /> <h1 className="App-title">Welcome to React</h1> </header> </div> ); } } export default App 76
  165. Everything is a module import React, { Component } from

    'react' import logo from './logo.svg' import './App.css' class App extends Component { render() { return ( <div className="App"> <header className="App-header"> <img src={logo} className="App-logo" alt="logo" /> <h1 className="App-title">Welcome to React</h1> </header> </div> ); } } export default App 76
  166. Webpack can load any type of file As long as

    you can provide a "loader" that tells how to convert the file into something the browser understands. This is how Webpack allows you to use Babel, TypeScript, Clojure, Elm, Imba but also to load CSSs, Images and other assets. 77
  167. { test: /\.css$/, use: [ require.resolve('style-loader'), { loader: require.resolve('css-loader'), options:

    { importLoaders: 1, }, }, { loader: require.resolve('postcss-loader'), options: { ident: 'postcss', plugins: () => [ require('postcss-flexbugs-fixes'), autoprefixer({ browsers: [ '>1%', 'last 4 versions', 'Firefox ESR', 'not ie < 9', 78
  168. { test: /\.css$/, use: [ require.resolve('style-loader'), { loader: require.resolve('css-loader'), options:

    { importLoaders: 1, }, }, { loader: require.resolve('postcss-loader'), options: { ident: 'postcss', plugins: () => [ require('postcss-flexbugs-fixes'), autoprefixer({ browsers: [ '>1%', 'last 4 versions', 'Firefox ESR', 'not ie < 9', // Defines how to load .css files (uses a pipeline of loaders) 78
  169. { test: /\.css$/, use: [ require.resolve('style-loader'), { loader: require.resolve('css-loader'), options:

    { importLoaders: 1, }, }, { loader: require.resolve('postcss-loader'), options: { ident: 'postcss', plugins: () => [ require('postcss-flexbugs-fixes'), autoprefixer({ browsers: [ '>1%', 'last 4 versions', 'Firefox ESR', 'not ie < 9', // Defines how to load .css files (uses a pipeline of loaders) // parses the file with post-css 78
  170. { test: /\.css$/, use: [ require.resolve('style-loader'), { loader: require.resolve('css-loader'), options:

    { importLoaders: 1, }, }, { loader: require.resolve('postcss-loader'), options: { ident: 'postcss', plugins: () => [ require('postcss-flexbugs-fixes'), autoprefixer({ browsers: [ '>1%', 'last 4 versions', 'Firefox ESR', 'not ie < 9', // Defines how to load .css files (uses a pipeline of loaders) // parses the file with post-css // process @import and url() // statements 78
  171. { test: /\.css$/, use: [ require.resolve('style-loader'), { loader: require.resolve('css-loader'), options:

    { importLoaders: 1, }, }, { loader: require.resolve('postcss-loader'), options: { ident: 'postcss', plugins: () => [ require('postcss-flexbugs-fixes'), autoprefixer({ browsers: [ '>1%', 'last 4 versions', 'Firefox ESR', 'not ie < 9', // Defines how to load .css files (uses a pipeline of loaders) // parses the file with post-css // process @import and url() // statements // inject the resulting code with a <style> tag 78
  172. ...Webpack can do (a lot) more! Dev Server Tree shaking

    Dependencies analytics Source maps Async require / module splitting @loige 79
  173. 1. Why we need modules 2. JavaScript module systems 3.

    How a module bundler works 4. Webpack in 2 minutes! 5. Advanced module bundling Agenda @loige 80
  174. Bundle cache busting Device CDN Origin Server bundle.js (original) 81

  175. Bundle cache busting Device CDN Origin Server example.com bundle.js (original)

    81
  176. Bundle cache busting Device CDN Origin Server example.com bundle.js bundle.js

    (original) 81
  177. Bundle cache busting Device CDN Origin Server example.com bundle.js bundle.js

    bundle.js (original) 81
  178. Bundle cache busting Device CDN Origin Server example.com bundle.js bundle.js

    bundle.js (original) 81
  179. Bundle cache busting Device CDN Origin Server example.com bundle.js bundle.js

    bundle.js (original) bundle.js (cache copy) 81
  180. Bundle cache busting Device CDN Origin Server example.com bundle.js bundle.js

    bundle.js (original) bundle.js (cache copy) 81
  181. Bundle cache busting Device CDN Origin Server example.com bundle.js bundle.js

    bundle.js (original) bundle.js (cache copy) 81 bundle.js (cache copy)
  182. Bundle cache busting Device CDN Origin Server example.com bundle.js bundle.js

    82 bundle.js (original) bundle.js (cache copy) bundle.js (cache copy)
  183. Bundle cache busting Device CDN Origin Server example.com bundle.js bundle.js

    82 bundle.js (original) bundle.js (cache copy) bundle.js (cache copy) NEW VERSION
  184. STALE! Bundle cache busting Device CDN Origin Server example.com bundle.js

    bundle.js 82 bundle.js (original) bundle.js (cache copy) bundle.js (cache copy) NEW VERSION STALE!
  185. Bundle cache busting 83

  186. Bundle cache busting (Manual) Solution 1 83

  187. Bundle cache busting (Manual) Solution 1 bundle.js?v=1 bundle.js?v=2 bundle.js?v=3 83

  188. Bundle cache busting (Manual) Solution 1 bundle.js?v=1 bundle.js?v=2 bundle.js?v=3 Doesn't

    play nice with some CDNs and Proxies (they won't consider different query parameters to be different resources) 83
  189. Bundle cache busting 84

  190. Bundle cache busting (Manual) Solution 2 84

  191. Bundle cache busting (Manual) Solution 2 bundle-v1.js bundle-v2.js bundle-v3.js ...

    84
  192. Bundle cache busting (Manual) Solution 2 bundle-v1.js bundle-v2.js bundle-v3.js ...

    Better, but still a lot of diligence and manual effort needed... 84
  193. Bundle cache busting 85

  194. Bundle cache busting Webpack Solution 85

  195. Bundle cache busting Webpack Solution output: { filename: '[name].[contenthash].js' }

    85
  196. Bundle cache busting Webpack Solution ... bundle.9f61f58dd1cc3bb82182.js bundle.aacdf58ef1aa12382199.js bundle.ed61f68defef3bb82221.js output:

    { filename: '[name].[contenthash].js' } 85
  197. Bundle cache busting Webpack Solution + Every new asset version

    will generate a new file cache is automatically cleaned up on every release (if content actually changed) html-plugin will update the reference to the new file contenthash webpack-html-plugin 86
  198. 1. Why we need modules 2. JavaScript module systems 3.

    How a module bundler works 4. Webpack in 2 minutes! 5. Advanced module bundling Agenda @loige 87
  199. Module bundlers are your friends Now you know how they

    work, they are not (really) magic! Start small and add more when needed If you try to build your own, you'll learn a lot more! @loige 88
  200. Webpack is not the only possibility! 89

  201. Grazie! Special thanks: , , , (reviewers) and (inspirations: his

    and his ) @Podgeypoos79 @andreaman87 @mariocasciaro @eugenserbanescu @MarijnJH amazing book workshop on JS modules @loige Images by Pug-Unicorn cover image by Background cover image by Streamline Emoji pack 1smr1 from Pixabay Gerd from Pixabay loige.link/bundle-coderful 90