เผยแพร่ จัดส่ง และติดตั้ง JavaScript ที่ทันสมัย เพื่อให้แอปพลิเคชันรวดเร็วขึ้น

ปรับปรุงประสิทธิภาพโดยการเปิดการพึ่งพาและเอาต์พุตของ JavaScript ที่ทันสมัย

เจสัน มิลเลอร์
เจสัน มิลเลอร์
ฮูเซน จอร์เดห์
ฮูสเซน จอร์เดห์

เบราว์เซอร์กว่า 90% สามารถเรียกใช้ JavaScript ที่ทันสมัยได้ แต่ความแพร่หลายของ JavaScript แบบเดิมยังคงเป็นแหล่งที่มาหลักของปัญหาด้านประสิทธิภาพบนเว็บในปัจจุบัน

JavaScript สมัยใหม่

JavaScript สมัยใหม่ไม่ได้มีลักษณะเป็นโค้ดที่เขียนขึ้นในเวอร์ชันข้อมูลจำเพาะ ECMAScript ที่เฉพาะเจาะจง แต่ใช้ไวยากรณ์ที่เบราว์เซอร์รุ่นใหม่ๆ ทั้งหมดรองรับ เว็บเบราว์เซอร์สมัยใหม่ เช่น Chrome, Edge, Firefox และ Safari รวมกันแล้วกว่า 90% ของตลาดเบราว์เซอร์ และเบราว์เซอร์ต่างๆ ที่ใช้เครื่องมือการแสดงผลพื้นฐานเดียวกันก็มีสัดส่วนเพิ่มขึ้น 5% ซึ่งหมายความว่า 95% ของการเข้าชมเว็บทั่วโลกมาจากเบราว์เซอร์ที่รองรับฟีเจอร์ภาษา JavaScript ที่ใช้กันอย่างแพร่หลายในช่วง 10 ปีที่ผ่านมา ดังนี้

  • ชั้นเรียน (ES2015)
  • ฟังก์ชันลูกศร (ES2015)
  • เครื่องกำเนิดไฟฟ้า (ES2015)
  • การจำกัดขอบเขต (ES2015)
  • การทำลาย (ES2015)
  • พารามิเตอร์การพักและการกระจาย (ES2015)
  • Object Shorthand (ES2015)
  • Async/await (ES2017)

โดยทั่วไปแล้ว ฟีเจอร์ในข้อกำหนดภาษาเวอร์ชันใหม่กว่าจะมีการสนับสนุนที่สม่ำเสมอน้อยกว่าในเบราว์เซอร์สมัยใหม่ ตัวอย่างเช่น ฟีเจอร์หลายรายการใน ES2020 และ ES2021 ได้รับการสนับสนุนเฉพาะใน 70% ของตลาดเบราว์เซอร์เท่านั้น ซึ่งยังคงเป็นเบราว์เซอร์ส่วนใหญ่ แต่ก็ไม่เพียงพอสำหรับการทำให้ฟีเจอร์ดังกล่าวใช้งานโดยตรงได้อย่างปลอดภัย ซึ่งหมายความว่าแม้ว่า JavaScript "สมัยใหม่" จะเป็นเป้าหมายที่เปลี่ยนแปลงไป แต่ ES2017 มีความเข้ากันได้กับเบราว์เซอร์ที่หลากหลายที่สุด ในขณะที่มีฟีเจอร์ไวยากรณ์ทันสมัยส่วนใหญ่ที่ใช้กันโดยทั่วไป กล่าวคือ ES2017 ใกล้เคียงกับไวยากรณ์สมัยใหม่มากที่สุดในปัจจุบัน

JavaScript เดิม

JavaScript เดิมเป็นโค้ดที่หลีกเลี่ยงการใช้ฟีเจอร์ภาษาข้างต้นทั้งหมดโดยเฉพาะ นักพัฒนาซอฟต์แวร์ส่วนใหญ่เขียนซอร์สโค้ดโดยใช้ไวยากรณ์สมัยใหม่ แต่รวบรวมทุกอย่างเป็นไวยากรณ์เดิมเพื่อให้รองรับเบราว์เซอร์ได้มากขึ้น การคอมไพล์เป็นไวยากรณ์แบบเดิมจะช่วยสนับสนุนเบราว์เซอร์มากขึ้น แต่ผลที่ได้นั้นมักจะน้อยกว่าที่เราคิดไว้ ในหลายกรณี การสนับสนุนจะเพิ่มขึ้นจากประมาณ 95% เป็น 98% ขณะที่มีค่าใช้จ่ายสูง ดังนี้

  • JavaScript เดิมมักมีขนาดใหญ่กว่าและช้ากว่าโค้ดสมัยใหม่ประมาณ 20% ข้อบกพร่องของเครื่องมือและการกำหนดค่าที่ไม่ถูกต้องมัก ขยายช่องว่างนี้ให้กว้างขึ้นไปอีก

  • ไลบรารีที่ติดตั้งใช้โค้ด JavaScript เวอร์ชันที่ใช้งานจริงทั่วไปได้มากถึง 90% ส่วนโค้ดไลบรารีมีค่ามากกว่า JavaScript เดิมเนื่องจาก Polyfill และการทำสำเนาตัวช่วย ซึ่งหลีกเลี่ยงได้ด้วยการเผยแพร่โค้ดสมัยใหม่

JavaScript สมัยใหม่ใน npm

เมื่อเร็วๆ นี้ Node.js ได้ปรับช่อง "exports" ให้เป็นมาตรฐานเพื่อกำหนดจุดแรกเข้าสำหรับแพ็กเกจ ดังนี้

{
  "exports": "./index.js"
}

โมดูลที่อ้างอิงโดยช่อง "exports" แสดงเวอร์ชันโหนดอย่างน้อย 12.8 ซึ่งรองรับ ES2019 ซึ่งหมายความว่าโมดูลใดก็ตามที่อ้างอิงโดยใช้ช่อง "exports" จะเขียนด้วย JavaScript สมัยใหม่ได้ ผู้ใช้แพ็กเกจต้องสมมติว่าโมดูลที่มีช่อง "exports" มีโค้ดที่ทันสมัยและเปลี่ยนรูปแบบหากจำเป็น

สมัยใหม่เท่านั้น

หากต้องการเผยแพร่แพ็กเกจที่มีรหัสทันสมัยและปล่อยให้ผู้บริโภคจัดการการเปลี่ยนรูปแบบเมื่อใช้เป็นทรัพยากร Dependency ให้ใช้ช่อง "exports" เท่านั้น

{
  "name": "foo",
  "exports": "./modern.js"
}

ทันสมัยพร้อมตัวเลือกแบบเดิม

ใช้ช่อง "exports" ร่วมกับ "main" เพื่อเผยแพร่แพ็กเกจโดยใช้รหัสสมัยใหม่ แต่ให้รวม ES5 + CommonJS สำรองสำหรับเบราว์เซอร์เดิมด้วย

{
  "name": "foo",
  "exports": "./modern.js",
  "main": "./legacy.cjs"
}

ทันสมัยพร้อมโฆษณาสำรองเดิมและการเพิ่มประสิทธิภาพ Bundler ของ ESM

นอกเหนือจากการกำหนดจุดแรกเข้า CommonJS สำรองแล้ว ช่อง "module" ยังใช้เพื่อชี้ไปยังแพ็กเกจสำรองแบบเดิมที่คล้ายกันได้ด้วย แต่เป็นแพ็กเกจที่ใช้ไวยากรณ์โมดูล JavaScript (import และ export)

{
  "name": "foo",
  "exports": "./modern.js",
  "main": "./legacy.cjs",
  "module": "./module.js"
}

Bundler หลายๆ รายการ เช่น Webpack และ Rollup อาศัยช่องนี้เพื่อใช้ประโยชน์จากฟีเจอร์โมดูลและเปิดใช้การเขย่าต้นไม้ ซึ่งยังคงเป็นแพ็กเกจเดิมที่ไม่มีโค้ดที่ทันสมัยนอกเหนือจากไวยากรณ์ import/export ดังนั้นให้ใช้แนวทางนี้เพื่อจัดส่งโค้ดที่ทันสมัยด้วยข้อมูลสำรองเดิมที่ยังคงเพิ่มประสิทธิภาพสําหรับการรวมแพ็กเกจ

JavaScript สมัยใหม่ในแอปพลิเคชัน

ทรัพยากร Dependency ของบุคคลที่สามเป็นโค้ด JavaScript เวอร์ชันที่ใช้งานจริงส่วนใหญ่ในเว็บแอปพลิเคชัน แม้ว่าการพึ่งพิง npm จะได้รับการเผยแพร่เป็นไวยากรณ์ ES5 เดิม แต่สิ่งนี้ไม่ใช่สมมติฐานที่ปลอดภัยอีกต่อไปและการอัปเดตการขึ้นต่อความเสี่ยงจะทำให้การสนับสนุนเบราว์เซอร์ในแอปพลิเคชันของคุณขัดข้อง

แพ็กเกจ npm ที่ย้ายไปยัง JavaScript ที่ทันสมัยมีจำนวนเพิ่มขึ้น คุณจึงต้องตั้งค่าเครื่องมือสร้างบิลด์ให้รองรับคำสั่งดังกล่าว มีโอกาสสูงที่แพ็กเกจ npm บางรายการที่คุณใช้จะใช้ฟีเจอร์ภาษาสมัยใหม่อยู่แล้ว มีตัวเลือกมากมายสำหรับการใช้โค้ดสมัยใหม่ตั้งแต่ npm โดยไม่ทำให้แอปพลิเคชันของคุณทำงานในเบราว์เซอร์รุ่นเก่า แต่แนวคิดทั่วไปคือการให้ทรัพยากร Dependency ของบิลด์ในระบบสร้างทรัพยากร Dependency ของไวยากรณ์เดียวกับซอร์สโค้ดของคุณ

Webpack

สำหรับ Webpack 5 ตอนนี้คุณสามารถกำหนดค่าชุดคำสั่งตามรูปแบบไวยากรณ์ที่จะใช้เมื่อสร้างโค้ดสำหรับแพ็กเกจและโมดูลได้แล้ว การดำเนินการนี้จะไม่เปลี่ยนรูปแบบของโค้ดหรือทรัพยากร Dependency แต่จะมีผลกับโค้ด "Glue" ที่สร้างขึ้นโดย Webpack เท่านั้น หากต้องการระบุเป้าหมายการสนับสนุนเบราว์เซอร์ ให้เพิ่มการกำหนดค่ารายการเบราว์เซอร์ลงในโปรเจ็กต์ หรือดำเนินการโดยตรงในการกำหนดค่าเว็บแพค ดังนี้

module.exports = {
  target: ['web', 'es2017'],
};

คุณยังสามารถกำหนดค่า Webpack เพื่อสร้างแพ็กเกจที่เพิ่มประสิทธิภาพซึ่งละเว้นฟังก์ชัน Wrapper ที่ไม่จำเป็นเมื่อกำหนดเป้าหมายสภาพแวดล้อมโมดูล ES ที่ทันสมัยได้ด้วย รวมถึงกำหนดค่า Webpack ให้โหลดแพ็กเกจที่แบ่งโค้ดโดยใช้ <script type="module"> ด้วย

module.exports = {
  target: ['web', 'es2017'],
  output: {
    module: true,
  },
  experiments: {
    outputModule: true,
  },
};

มีปลั๊กอิน Webpack ให้ใช้งานจำนวนหนึ่งที่ช่วยให้คอมไพล์และจัดส่ง JavaScript สมัยใหม่ได้ในขณะที่ยังคงรองรับเบราว์เซอร์รุ่นเก่าได้ เช่น Optimize ปลั๊กอิน และ BabelEsmPlugin

ปลั๊กอิน Optimize

ปลั๊กอิน Optimize คือปลั๊กอิน Webpack ที่เปลี่ยนรูปแบบโค้ดแพ็กเกจสุดท้ายจากแบบโมเดิร์นเป็น JavaScript แบบเดิมแทนไฟล์ต้นทางแต่ละไฟล์ นี่เป็นการตั้งค่าในตัว ซึ่งช่วยให้การกำหนดค่า Webpack ของคุณถือว่าทุกอย่างเป็น JavaScript สมัยใหม่ โดยไม่มีการแตกสาขาพิเศษสำหรับเอาต์พุตหรือไวยากรณ์หลายรายการ

เนื่องจาก Optimize Plugin จะทำงานใน Bundle ไม่ใช่แต่ละโมดูล จึงประมวลผลโค้ดของแอปพลิเคชันและ Dependencies ของคุณเท่าๆ กัน วิธีนี้จะช่วยให้การใช้ทรัพยากร Dependency ของ JavaScript สมัยใหม่จาก npm ได้อย่างปลอดภัย เนื่องจากโค้ดของทรัพยากรดังกล่าวจะถูกจัดกลุ่มและเปลี่ยนรูปแบบเป็นไวยากรณ์ที่ถูกต้อง ทั้งยังรวดเร็วกว่าโซลูชันแบบดั้งเดิมที่เกี่ยวข้องกับขั้นตอนการรวบรวม 2 ขั้นตอน ในขณะที่ยังคงสร้างแพ็กเกจแยกกันสำหรับเบราว์เซอร์สมัยใหม่และเบราว์เซอร์เดิม แพ็กเกจทั้ง 2 ชุดออกแบบมาให้โหลดโดยใช้รูปแบบโมดูล/ไม่มีโมดูล

// webpack.config.js
const OptimizePlugin = require('optimize-plugin');

module.exports = {
  // ...
  plugins: [new OptimizePlugin()],
};

Optimize Plugin อาจเร็วและมีประสิทธิภาพกว่าการกำหนดค่า Webpack ที่กำหนดเอง ซึ่งมักจะรวมโค้ดสมัยใหม่และโค้ดเดิมไว้แยกกัน รวมถึงยังจัดการกับการเรียกใช้ Babel ให้คุณและลดขนาดแพ็กเกจโดยใช้ Terser ด้วยการตั้งค่าที่เหมาะสมที่สุดแยกกันสำหรับเอาต์พุตแบบเดิมและแบบใหม่ สุดท้าย ระบบจะแยก Polyfill ที่แพ็กเกจเดิมที่สร้างขึ้นต้องใช้แยกออกมาเป็นสคริปต์เฉพาะเพื่อไม่ให้ระบบสร้าง Polyfill ที่ซ้ำกันหรือโหลดโดยไม่จำเป็นในเบราว์เซอร์ใหม่ๆ

การเปรียบเทียบ: การเปลี่ยนรูปแบบโมดูลแหล่งที่มา 2 ครั้งเทียบกับการเปลี่ยนรูปแบบแพ็กเกจที่สร้างขึ้น

BabelEsmPlugin

BabelEsmPlugin เป็นปลั๊กอิน Webpack ที่ทำงานร่วมกับ @babel/preset-env เพื่อสร้างแพ็กเกจที่มีอยู่เวอร์ชันล่าสุดเพื่อส่งโค้ดที่เปลี่ยนรูปแบบน้อยลงไปยังเบราว์เซอร์ที่ทันสมัย และเป็นโซลูชันสำเร็จรูปที่ได้รับความนิยมมากที่สุดสำหรับโมดูล/nomodule ซึ่ง Next.js และ Preact CLI ใช้

// webpack.config.js
const BabelEsmPlugin = require('babel-esm-plugin');

module.exports = {
  //...
  module: {
    rules: [
      // your existing babel-loader configuration:
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env'],
          },
        },
      },
    ],
  },
  plugins: [new BabelEsmPlugin()],
};

BabelEsmPlugin รองรับการกำหนดค่า Web Pack ได้อย่างหลากหลาย เนื่องจากมีการใช้งานแอปพลิเคชันของคุณ 2 รุ่นแยกกันเป็นส่วนใหญ่ การคอมไพล์ 2 ครั้งอาจทำให้ใช้เวลาเพิ่มขึ้นเล็กน้อยสำหรับแอปพลิเคชันขนาดใหญ่ แต่เทคนิคนี้ช่วยให้ BabelEsmPlugin ผสานรวมกับการกำหนดค่า Webpack ที่มีอยู่ได้อย่างราบรื่น และทำให้เป็นหนึ่งในตัวเลือกที่สะดวกที่สุดที่มี

กำหนดค่า Babel-loader เพื่อเปลี่ยนรูปแบบ Node_modules

หากใช้ babel-loader โดยไม่มีปลั๊กอิน 2 รายการก่อนหน้านี้ จะมีขั้นตอนสำคัญที่จำเป็นต่อการใช้โมดูล NPM ของ JavaScript ที่ทันสมัย การกำหนดการกำหนดค่า babel-loader ที่แยกกัน 2 รายการช่วยให้รวบรวมฟีเจอร์ภาษาสมัยใหม่ที่พบใน node_modules ถึง ES2017 ได้โดยอัตโนมัติ ในขณะที่ยังคงเปลี่ยนรูปแบบโค้ดของบุคคลที่หนึ่งของคุณเองด้วยปลั๊กอิน Babel และค่าที่กำหนดล่วงหน้าที่กำหนดไว้ในการกำหนดค่าโปรเจ็กต์ได้ การดำเนินการนี้จะไม่สร้างแพ็กเกจเดิมและทันสมัยสำหรับการตั้งค่าโมดูล/ไม่มีโมดูล แต่ช่วยให้ติดตั้งและใช้แพ็กเกจ npm ที่มี JavaScript ที่ทันสมัยโดยไม่ทำให้เบราว์เซอร์เก่าเสียหายได้

webpack-plugin-modern-npm ใช้เทคนิคนี้เพื่อคอมไพล์ทรัพยากร Dependency ของ NPM ที่มีช่อง "exports" ใน package.json เนื่องจากอาจมีไวยากรณ์ทันสมัยดังนี้

// webpack.config.js
const ModernNpmPlugin = require('webpack-plugin-modern-npm');

module.exports = {
  plugins: [
    // auto-transpile modern stuff found in node_modules
    new ModernNpmPlugin(),
  ],
};

หรือคุณจะใช้เทคนิคนี้ด้วยตนเองในการกำหนดค่า WebP ก็ได้ โดยตรวจสอบช่อง "exports" ใน package.json ของโมดูลเมื่อได้รับการแก้ไข การไม่แคชเพื่อความสั้นกระชับ การใช้งานที่กำหนดเองอาจมีลักษณะดังนี้

// webpack.config.js
module.exports = {
  module: {
    rules: [
      // Transpile for your own first-party code:
      {
        test: /\.js$/i,
        loader: 'babel-loader',
        exclude: /node_modules/,
      },
      // Transpile modern dependencies:
      {
        test: /\.js$/i,
        include(file) {
          let dir = file.match(/^.*[/\\]node_modules[/\\](@.*?[/\\])?.*?[/\\]/);
          try {
            return dir && !!require(dir[0] + 'package.json').exports;
          } catch (e) {}
        },
        use: {
          loader: 'babel-loader',
          options: {
            babelrc: false,
            configFile: false,
            presets: ['@babel/preset-env'],
          },
        },
      },
    ],
  },
};

เมื่อใช้วิธีนี้ ก็ต้องตรวจสอบว่าตัวขยายรองรับไวยากรณ์สมัยใหม่แล้ว ทั้ง Terser และ uglify-es มีตัวเลือกในการระบุ {ecma: 2017} เพื่อเก็บรักษา และในบางกรณีจะสร้างไวยากรณ์ ES2017 ระหว่างการบีบอัดและการจัดรูปแบบ

รายงาน

ภาพรวมมีการรองรับในตัวสำหรับการสร้างแพ็กเกจหลายชุดโดยเป็นส่วนหนึ่งของบิลด์เดียว และสร้างโค้ดที่ทันสมัยโดยค่าเริ่มต้น ด้วยเหตุนี้ คุณจึงกำหนดค่า Rollup เพื่อสร้างแพ็กเกจเดิมและสมัยใหม่ด้วยปลั๊กอินอย่างเป็นทางการที่คุณน่าจะใช้อยู่แล้ว

@rollup/plugin-babel

หากคุณใช้ Rollup เมธอด getBabelOutputPlugin() (ที่ได้จากปลั๊กอิน Babel อย่างเป็นทางการของ Rollup) จะเปลี่ยนรูปแบบโค้ดในแพ็กเกจที่สร้างขึ้นแทนโมดูลแหล่งที่มาแต่ละรายการ รายงานมีการรองรับในตัวสำหรับการสร้างแพ็กเกจหลายชุดเพื่อเป็นส่วนหนึ่งของบิลด์เดียว โดยแต่ละบิลด์มีปลั๊กอินของตัวเอง คุณสามารถใช้ปลั๊กอินนี้สร้างแพ็กเกจที่ต่างกันสำหรับแพ็กเกจสมัยใหม่และแพ็กเกจเดิมได้โดยส่งผ่านการกำหนดค่าปลั๊กอินเอาต์พุต Babel ที่ต่างกัน ดังนี้

// rollup.config.js
import {getBabelOutputPlugin} from '@rollup/plugin-babel';

export default {
  input: 'src/index.js',
  output: [
    // modern bundles:
    {
      format: 'es',
      plugins: [
        getBabelOutputPlugin({
          presets: [
            [
              '@babel/preset-env',
              {
                targets: {esmodules: true},
                bugfixes: true,
                loose: true,
              },
            ],
          ],
        }),
      ],
    },
    // legacy (ES5) bundles:
    {
      format: 'amd',
      entryFileNames: '[name].legacy.js',
      chunkFileNames: '[name]-[hash].legacy.js',
      plugins: [
        getBabelOutputPlugin({
          presets: ['@babel/preset-env'],
        }),
      ],
    },
  ],
};

เครื่องมือสร้างเพิ่มเติม

Rollup และ Webpack ที่กำหนดค่าได้อย่างสูง ซึ่งหมายความว่าโดยทั่วไปแล้วแต่ละโปรเจ็กต์จะต้องอัปเดตการกำหนดค่าเพื่อเปิดใช้ไวยากรณ์ JavaScript สมัยใหม่ในทรัพยากร Dependency นอกจากนี้ ยังมีเครื่องมือสร้างระดับสูงซึ่งรองรับแบบแผนและค่าเริ่มต้นมากกว่าการกำหนดค่า เช่น Parcel, Snowpack, Vite และ WMR เครื่องมือเหล่านี้ส่วนใหญ่สันนิษฐานว่าทรัพยากร Dependency ของ npm อาจมีไวยากรณ์ที่ทันสมัย และจะเปลี่ยนรูปแบบไวยากรณ์เหล่านั้นให้เป็นระดับไวยากรณ์ที่เหมาะสมเมื่อสร้างสำหรับเวอร์ชันที่ใช้งานจริง

นอกเหนือจากปลั๊กอินเฉพาะสำหรับ Webpack และ Rollup แล้ว คุณยังเพิ่มแพ็กเกจ JavaScript สมัยใหม่ที่มีโฆษณาสำรองเดิมไปยังโปรเจ็กต์ใดก็ได้โดยใช้การพัฒนาซอฟต์แวร์ Devolution เป็นเครื่องมือแบบสแตนด์อโลนที่แปลงเอาต์พุตจากระบบบิลด์เพื่อสร้างตัวแปร JavaScript เดิม ซึ่งช่วยให้การรวมและเปลี่ยนรูปแบบเป็นเป้าหมายของเอาต์พุตที่ทันสมัยได้