laravel-mix / laravel-mix

The power of webpack, distilled for the rest of us.
MIT License
5.27k stars 808 forks source link

50 seconds compile time with PostCSS and Tailwind #2470

Closed gopeter closed 4 years ago

gopeter commented 4 years ago

Description:

Initial compilation of my app.css takes 50-60 seconds, changes to it need around 40-50 seconds (even if I just press Save again). Removing the Tailwind directives in the app.css makes it fast again, but I thought that these would be incremental builds. Is there any misconfiguration on my side?

Steps To Reproduce:

app.css:

@tailwind base;
@tailwind components;
@tailwind utilities;

And that's my webpack.mix.js file (based on https://github.com/ben-rogerson/agency-webpack-mix-config, but with a few changes):

/**
 * ===========================
 * Agency Webpack-Mix Config
 * A capable website/webapp config built for the modern web agency.
 * https://github.com/ben-rogerson/agency-webpack-mix-config
 * ===========================
 *
 * Contents
 *
 * 🎚️ Settings
 * 🏠 Templates
 * 🎭 Hashing
 * 🎨 Styles: PostCSS
 * 🎨 Styles: CriticalCSS
 * 🎨 Styles: PurgeCSS
 * 🎨 Styles: Polyfills
 * πŸ“‘ Scripts
 * πŸ“‘ Scripts: Polyfills
 * πŸ“‘ Scripts: Auto import libraries
 * πŸ“‘ Scripts: Vendor
 * πŸ“‘ Scripts: Linting
 * 🏞 Images
 * πŸŽ† Icons
 * πŸ—‚οΈ Static
 * 🚧 Webpack-dev-server
 */

// 🎚️ Base config
const config = {
  // Dev domain to proxy
  devProxyDomain: process.env.DEFAULT_SITE_URL || 'https://myproject.test',
  // Paths to observe for changes then trigger a full page reload
  devWatchPaths: ['templates'],
  // Port to use with webpack-dev-server
  devServerPort: 8080,
  // Folders where purgeCss can look for used selectors
  purgeCssGrabFolders: ['src', 'templates'],
  // Build a static site from the src/template files
  buildStaticSite: false,
  // Urls for CriticalCss to look for "above the fold" Css
  criticalCssUrls: [
    // { urlPath: "/", label: "index" },
    // { urlPath: "/about", label: "about" },
  ],
  // Folder served to users
  publicFolder: 'web/assets',
}

// 🎚️ Imports
require('laravel-mix-react-typescript-extension')
const mix = require('laravel-mix')
const path = require('path')
const globby = require('globby')
const tailwindcss = require('tailwindcss')
const autoprefixer = require('autoprefixer')
const presetenv = require('postcss-preset-env')
const hexrgba = require('postcss-hexrgba')

// 🎚️ Source folders
const source = {
  icons: path.resolve('src/icons'),
  images: path.resolve('src/images'),
  scripts: path.resolve('src/scripts'),
  styles: path.resolve('src/styles'),
  static: path.resolve('src/static'),
  templates: path.resolve('templates'),
}

// 🎚️ Misc
mix.setPublicPath(config.publicFolder)
mix.disableNotifications()
mix.webpackConfig({ resolve: { alias: source } })
!mix.inProduction() && mix.sourceMaps()

/**
 * 🎭 Hashing (for non-static sites)
 * Mix has querystring hashing by default, eg: main.css?id=abcd1234
 * This script converts it to filename hashing, eg: main.abcd1234.css
 * https://github.com/JeffreyWay/laravel-mix/issues/1022#issuecomment-379168021
 */
if (mix.inProduction() && !config.buildStaticSite) {
  // Allow versioning in production
  mix.version()
  // Get the manifest filepath for filehash conversion
  const manifestPath = path.join(config.publicFolder, 'mix-manifest.json')
  // Run after mix finishes
  mix.then(() => {
    const convertToFileHash = require('laravel-mix-make-file-hash')
    convertToFileHash({
      publicPath: config.publicFolder,
      manifestFilePath: manifestPath,
    })
  })
}

/**
 * 🎨 Styles: PostCSS
 * Extend Css with plugins
 * https://laravel-mix.com/docs/4.0/css-preprocessors#postcss-plugins
 */
mix.postCss(path.join(source.styles, 'app.css'), 'css').options({
  postCss: [
    tailwindcss(),
    autoprefixer({
      cascade: false,
    }),
    presetenv({
      stage: 0,
    }),
    hexrgba,
  ],
  processCssUrls: false,
})

/**
 * 🎨 Styles: CriticalCSS
 * https://github.com/addyosmani/critical#options
 */
const criticalDomain = config.devProxyDomain
if (criticalDomain && config.criticalCssUrls && config.criticalCssUrls.length) {
  require('laravel-mix-critical')
  const url = require('url')
  mix.critical({
    enabled: mix.inProduction(),
    urls: config.criticalCssUrls.map((page) => ({
      src: url.resolve(criticalDomain, page.urlPath),
      dest: path.join(config.publicFolder, 'css', `${page.label}-critical.css`),
    })),
    options: {
      width: 1200,
      height: 1200,
    },
  })
}

/**
 * 🎨 Styles: PurgeCSS
 * https://github.com/spatie/laravel-mix-purgecss#usage
 */
if (config.purgeCssGrabFolders.length) {
  require('laravel-mix-purgecss')
  mix.purgeCss({
    enabled: mix.inProduction(),
    folders: config.purgeCssGrabFolders, // Folders scanned for selectors
    whitelist: ['html', 'body', 'active', 'wf-active', 'wf-inactive'],
    whitelistPatterns: [],
    extensions: ['php', 'twig', 'html', 'js', 'mjs', 'ts', 'tsx'],
  })
}

/**
 * πŸ“‘ Scripts: Main
 * Script files are transpiled to vanilla JavaScript
 * https://laravel-mix.com/docs/4.0/mixjs
 */
const scriptFiles = globby.sync(`${source.scripts}/*.{js,mjs,ts,tsx}`)
scriptFiles.forEach((scriptFile) => {
  mix.reactTypeScript(scriptFile, path.join(config.publicFolder, 'js'))
})

/**
 * πŸ“‘ Scripts: Polyfills
 * Automatically add polyfills for target browsers with core-js@3
 * See "browserslist" in package.json for your targets
 * https://github.com/zloirock/core-js/blob/master/docs/2019-03-19-core-js-3-babel-and-a-look-into-the-future.md
 * https://github.com/scottcharlesworth/laravel-mix-polyfill#options
 */
require('laravel-mix-polyfill')
mix.polyfill({
  enabled: mix.inProduction(),
  useBuiltIns: 'usage', // Only add a polyfill when a feature is used
  targets: false, // "false" makes the config use browserslist targets in package.json
  corejs: 3,
  debug: false, // "true" to check which polyfills are being used
})

/**
 * πŸ“‘ Scripts: Vendor
 * Separate the JavaScript code imported from node_modules
 * https://laravel-mix.com/docs/4.0/extract
 * Without mix.extract you'll see an annoying js error after
 * launching the dev server - this should be fixed in webpack 5
 */
mix.extract([], path.join(config.publicFolder, 'js', 'vendor')) // Empty params = separate all node_modules
// mix.extract(['jquery']) // Specify packages to add to the vendor file

/**
 * πŸ“‘ Scripts: Linting
 */
if (!mix.inProduction()) {
  require('laravel-mix-eslint')
  mix.eslint()
}

/**
 * 🏞 Images
 * Images are optimized and copied to the build directory
 * https://github.com/CupOfTea696/laravel-mix-imagemin
 * https://github.com/Klathmon/imagemin-webpack-plugin#api
 *
 * Important: laravel-mix-imagemin is incompatible with
 * copy-webpack-plugin > 5.1.1, so keep that dependency at that version.
 * See: https://github.com/CupOfTea696/laravel-mix-imagemin/issues/9
 */
require('laravel-mix-imagemin')
mix.imagemin(
  {
    from: path.join(source.images, '**/*'),
    to: config.publicFolder,
    context: 'src/images',
  },
  {},
  {
    gifsicle: { interlaced: true },
    mozjpeg: { progressive: true, arithmetic: false },
    optipng: { optimizationLevel: 3 }, // Lower number = speedier/reduced compression
    svgo: {
      plugins: [
        { convertPathData: false },
        { convertColors: { currentColor: false } },
        { removeDimensions: true },
        { removeViewBox: false },
        { cleanupIDs: false },
      ],
    },
  },
)

/**
 * πŸŽ† Icons
 * Individual SVG icons are optimised then combined into a single cacheable SVG
 * https://github.com/kisenka/svg-sprite-loader#configuration
 */
require('laravel-mix-svg-sprite')
mix.svgSprite(source.icons, path.join(config.publicFolder, 'sprite.svg'), {
  symbolId: (filePath) => `icon-${path.parse(filePath).name}`,
  extract: true,
})

// Icon options
mix.options({
  imgLoaderOptions: {
    svgo: {
      plugins: [{ convertColors: { currentColor: true } }, { removeDimensions: false }, { removeViewBox: false }],
    },
  },
})

/**
 * πŸ—‚οΈ Static
 * Additional folders with no transform requirements are copied to your build folders
 */
mix.copyDirectory(source.static, path.join(config.publicFolder))

/**
 * 🚧 Webpack-dev-server
 * https://webpack.js.org/configuration/dev-server/
 */
mix.webpackConfig({
  devServer: {
    clientLogLevel: 'none', // Hide console feedback so eslint can take over
    open: true,
    overlay: true,
    port: config.devServerPort,
    public: `localhost:${config.devServerPort}`,
    host: '0.0.0.0', // Allows access from network
    https: config.devProxyDomain.includes('https://'),
    contentBase: config.devWatchPaths.length ? config.devWatchPaths : undefined,
    watchContentBase: config.devWatchPaths.length > 0,
    watchOptions: {
      aggregateTimeout: 200,
      poll: 200, // Lower for faster reloads (more cpu intensive)
      ignored: ['storage', 'node_modules', 'vendor'],
    },
    disableHostCheck: true, // Fixes "Invalid Host header error" on assets
    headers: {
      'Access-Control-Allow-Origin': '*',
    },
    proxy: {
      '**': {
        target: config.devProxyDomain,
        changeOrigin: true,
        secure: false,
      },
    },
    publicPath: '/',
  },
})

mix.options({
  hmrOptions: {
    host: 'localhost',
    port: config.devServerPort,
  },
})
saltymouse commented 4 years ago

Same slow build times here. Related to other open issues: https://github.com/tailwindlabs/tailwindcss/discussions/1514 and https://github.com/JeffreyWay/laravel-mix/issues/2411

torian257x commented 4 years ago

this kinda helped me: https://gist.github.com/Jossnaz/7cf182e794e515d068159ad71fcf7855

I use yarn hot to build tailwind is set to watch. And incremental watch builds seem to work initial build time is still almost 30s or so. BUT: the tailwind build runs in a separate process, thus you are free to go coding right away.

basically concurrently or whatever the lib is called runs hot and watch in separate process

as well: tailwind is split into utilities and your own stuff that way you dont have to rebuild utilities on each change

so its 2 things that help DX 2 process split tailwind files

danimalweb commented 4 years ago

Same slow build times here.

Ahrengot commented 4 years ago

By splitting Tailwind utilities from the rest of the stylesheet, i got down from 26 seconds on every file change to 0.15 seconds. You pretty much get instant feedback.

The initial build still takes 26 seconds because it needs to compile all of those thousands of lines of utility classes, but the subsequent builds a really fast and that's what matters.

Here's my setup

webpack.mix.js

const postCssPlugins = [
  require("postcss-import")({
    from: "resources/css/app.css"
  }),

  require("tailwindcss"),

  require("postcss-preset-env")({
    stage: 0
  }),
];

mix
  .postCss("resources/css/app.css", "public/css", postCssPlugins)
  .postCss(
    "resources/css/tailwind-utilities.css",
    "public/css",
    postCssPlugins
  );

app.css

@import 'tailwindcss/base';
@import 'tailwindcss/components';

/* Custom components */
@import 'components/button';

tailwind-utilities.css

@import "tailwindcss/utilities";

app-layout.blade.php

<link rel="stylesheet" href="{{ mix('css/app.css') }}">
<link rel="stylesheet" href="{{ mix('css/tailwind-utilities.css') }}">

This works great. If you want just one compiled css file, you can use mix.combine, but I like to keep them seperate β€” At least during development. It makes it a little easier if you don't have 20k lines of utility classes mixed in with your actual CSS.

JeffreyWay commented 4 years ago

It’s hard to debug such a massive mix configuration file. To help, I’d need you to break this down to the simplest reproducible example.

Also check Mix 6 beta. That may have resolved the issue inadvertently.