A Modern Gulp 4 Workflow with ESBuild, Sass, and BrowserSync (Node 20 Ready)

This guide outlines a modern frontend workflow using Gulp 4, ESBuild, Sass, and BrowserSync. It is designed to work with Node.js 18 or 20 and provides a fast, maintainable setup for building web projects.

A step-by-step guide to setting up a fast, future-proof frontend workflow for modern web projects.

For years, many frontend projects relied on outdated build tools that only worked with old Node versions. In this guide, we’ll build a modern Gulp 4 workflow that works perfectly with Node.js 20, uses ESBuild for JavaScript, Dart Sass for CSS, and BrowserSync for live reloading.

This setup is ideal for:

  • Static websites
  • Marketing sites
  • WordPress themes (assets-only)
  • Long-term maintainable projects

What This Workflow Includes

  • Gulp 4 (task runner)
  • ESBuild (fast JS bundler)
  • Dart Sass (modern Sass compiler)
  • BrowserSync (live reload)
  • Separate app and vendor assets
  • Support for:
    • HTML
    • CSS (Sass)
    • JavaScript
    • Images
    • Fonts
    • Videos
  • Fully compatible with Node.js 18+ / 20

Recommended Project Structure

project-root/
├── src/
│   ├── scss/
│   │   ├── app.scss
│   │   └── vendor.scss
│   ├── js/
│   │   ├── app.js
│   │   └── vendor.js
│   ├── images/
│   ├── fonts/
│   ├── videos/
│   └── index.html
├── dist/
├── gulpfile.js
├── package.json
└── node_modules/

Step 1: Install Node.js

Make sure you are running Node.js 18 or 20. Download Node.js from https://nodejs.org

node -v

Recommended:

  • Node 20 LTS

Step 2: Install Dependencies

Initialize your project:

npm init -y

Install required packages:

npm install --save-dev \
gulp gulp-cli \
browser-sync \
esbuild \
sass gulp-sass \
gulp-autoprefixer \
gulp-clean-css \
gulp-sourcemaps

Step 3: package.json (example)

{
  "name": "modern-gulp-workflow",
  "private": true,
  "devDependencies": {
    "browser-sync": "^3.0.2",
    "esbuild": "^0.20.2",
    "gulp": "^4.0.2",
    "gulp-autoprefixer": "^8.0.0",
    "gulp-clean-css": "^4.3.0",
    "gulp-sass": "^5.1.0",
    "gulp-sourcemaps": "^3.0.0",
    "sass": "^1.70.0"
  },
  "browserslist": [
    "> 0.5%",
    "last 2 versions",
    "not dead"
  ]
}

Step 4: Create gulpfile.js

This is the complete production-ready configuration.

const { src, dest, watch, series, parallel } = require('gulp');
const dartSass = require('sass');
const gulpSass = require('gulp-sass');
const sass = gulpSass(dartSass);

const autoprefixer = require('gulp-autoprefixer');
const cleanCSS = require('gulp-clean-css');
const sourcemaps = require('gulp-sourcemaps');
const browserSync = require('browser-sync').create();
const esbuild = require('esbuild');
const fs = require('fs');
const path = require('path');

// Paths
const paths = {
  html: 'src/**/*.html',
  styles: {
    app: 'src/scss/app.scss',
    vendor: 'src/scss/vendor.scss'
  },
  scripts: {
    app: 'src/js/app.js',
    vendor: 'src/js/vendor.js'
  },
  images: 'src/images/**/*',
  fonts: 'src/fonts/**/*',
  videos: 'src/videos/**/*',
  dist: 'dist'
};

// Clean dist folder
function clean(cb) {
  const distPath = path.resolve(paths.dist);
  if (fs.existsSync(distPath)) {
    fs.rmSync(distPath, { recursive: true, force: true });
  }
  cb();
}

// HTML
function html() {
  return src(paths.html)
    .pipe(dest(paths.dist))
    .pipe(browserSync.stream());
}

// Styles
function styles() {
  return src([paths.styles.app, paths.styles.vendor], { allowEmpty: true })
    .pipe(sourcemaps.init())
    .pipe(
      sass({
        outputStyle: 'expanded',
        silenceDeprecations: [
          'legacy-js-api',
          'import',
          'if-function',
          'global-builtin',
          'color-functions'
        ]
      }).on('error', sass.logError)
    )
    .pipe(autoprefixer())
    .pipe(cleanCSS())
    .pipe(sourcemaps.write('.'))
    .pipe(dest(`${paths.dist}/css`))
    .pipe(browserSync.stream());
}

// Scripts (ESBuild)
function scripts(cb) {
  Promise.all([
    esbuild.build({
      entryPoints: [paths.scripts.app],
      bundle: true,
      minify: true,
      sourcemap: true,
      outfile: `${paths.dist}/js/app.js`,
      target: ['es2018']
    }),
    esbuild.build({
      entryPoints: [paths.scripts.vendor],
      bundle: true,
      minify: true,
      sourcemap: true,
      outfile: `${paths.dist}/js/vendor.js`,
      target: ['es2018']
    })
  ])
    .then(() => {
      browserSync.reload();
      cb();
    })
    .catch(err => {
      console.error(err.message);
      cb();
    });
}

// Assets
const images = () => src(paths.images).pipe(dest(`${paths.dist}/images`)).pipe(browserSync.stream());
const fonts  = () => src(paths.fonts).pipe(dest(`${paths.dist}/fonts`)).pipe(browserSync.stream());
const videos = () => src(paths.videos).pipe(dest(`${paths.dist}/videos`)).pipe(browserSync.stream());

// Serve
function serve() {
  browserSync.init({
    server: { baseDir: paths.dist },
    open: false,
    notify: false
  });

  watch(paths.html, html);
  watch('src/scss/**/*.scss', styles);
  watch('src/js/**/*.js', scripts);
  watch(paths.images, images);
  watch(paths.fonts, fonts);
  watch(paths.videos, videos);
}

// Tasks
exports.build = series(clean, parallel(html, styles, scripts, images, fonts, videos));
exports.default = series(exports.build, serve);

Step 5: Run the Project

npx gulp

BrowserSync will start at:

http://localhost:3000

Best Practices for the Team

  • One SCSS entry = one CSS file
  • Keep third-party code in vendor.scss and vendor.js
  • Do not compile partial SCSS files directly
  • Do not modify files inside node_modules
  • Always run npx gulp (avoid global version issues)

Why This Workflow Works

  • Fast: ESBuild is extremely fast
  • Stable: No ESM/CommonJS conflicts
  • Future-proof: Compatible with modern Node & Sass
  • Flexible: Works for static sites and CMS projects
  • Team-friendly: Clear separation of responsibilities

Final Result

dist/
├── css/
│   ├── app.css
│   └── vendor.css
├── js/
│   ├── app.js
│   └── vendor.js
├── images/
├── fonts/
├── videos/
└── index.html

Adding Bootstrap 5 to the Workflow (Optional)

Bootstrap 5 can be added cleanly to this workflow using NPM + Sass + ESBuild, without relying on CDNs or legacy tooling.

This approach allows:

  • Better caching
  • Smaller bundles
  • Full control over styles
  • Easy removal later if needed

Step 1: Install Bootstrap 5

From your project root, run:

npm install bootstrap@5

This installs:

  • Bootstrap Sass source files
  • Bootstrap JavaScript (no jQuery required)

Step 2: Add Bootstrap CSS (Sass)

Open

src/scss/vendor.scss

Add only this line:

/* src/scss/vendor.scss */
@use "../../node_modules/bootstrap/scss/bootstrap";

Important Notes

  • Do not import Bootstrap files individually
  • Do not use @import
  • Bootstrap must be loaded as a single Sass entry

This ensures all internal mixins and variables work correctly.

Step 3: Add Bootstrap JavaScript

Open

src/js/vendor.js

Add

// src/js/vendor.js
import 'bootstrap';

ESBuild will automatically bundle:

  • Modals
  • Dropdowns
  • Collapse
  • Tooltips
  • Popovers
  • And other JS components

Step 4: Load Vendor Files in HTML

Make sure vendor files load before app files.

<link rel="stylesheet" href="css/vendor.css">
<link rel="stylesheet" href="css/app.css">

<script src="js/vendor.js"></script>
<script src="js/app.js"></script>

Step 5: Run the Build

npx gulp

Removing Bootstrap Later (Optional)

If Bootstrap is no longer needed:

npm uninstall bootstrap

Then remove:

  • The @use line in vendor.scss
  • The import 'bootstrap' line in vendor.js

Rebuild:

npx gulp

Reach out to our team today

If you’re looking for an experienced web development agency in the Philippines to build or optimize your next project, reach out to our team today.