Available For Front-End & Design Systems Contracts – Get In Touch

Always Twisted

I’ve been compiling my Sass wrong, for years

For years, I've been preprocessing Sass in my build pipeline without really thinking about how I was doing it. Every project would follow the same pattern: set up Sass, write a build script, and leave it alone. It worked. It was fast enough. I never questioned it.

That changed recently when I saw a deprecation warning in my build logs. It transpires, I've been using an API that's been obsolete for almost five years. And I didn't even know it.

The warning

The warning was clear enough:

Code languagebash
Deprecation Warning: The legacy JS API is deprecated and will be removed in Dart Sass 2.0.0.

I'd probably glanced over this warning a couple of times in recent weeks, but today it stopped me. I'm using Dart Sass. How was I possibly using a "legacy" API? I started digging and realised I've been clinging to renderSync() since my node-sass days.

What I missed, the modern API

Back in late 2021, Dart Sass 1.45.0 introduced a fresh approach. Instead of renderSync(), the new API gave us:

These aren't just renamed methods. They're a proper redesign with better performance, cleaner syntax, proper Promise support instead of callbacks, and better TypeScript support.

The performance bump

The real surprise is the performance gain. When you use the modern API with sass-embedded (the faster Dart Sass implementation), you could be looking at 5-10x faster compilation. That's not a marginal improvement. For projects with hundreds of imports, that's the difference between a build taking 30 seconds and 3 seconds.

Why? The old API was designed for the node-sass era, before modern Sass had its own compiler. The new API was built from the ground up for Dart Sass.

Migrating the code

Here's what the actual migration looks like. My old code using the legacy API:

Code languagejavascript
const sass = require('sass');
const fs = require('fs');

// Synchronous file compilation
const result = sass.renderSync({
  file: 'styles/main.scss',
  outputStyle: 'compressed',
  sourceMap: true,
  outFile: 'dist/main.css',
});

fs.writeFileSync('dist/main.css', result.css);
fs.writeFileSync('dist/main.css.map', result.map);

The modern equivalent:

Code languagejavascript
const sass = require('sass');
const fs = require('fs');

// Synchronous file compilation
const result = sass.compile('styles/main.scss', {
  style: 'compressed',
  sourceMap: true,
});

fs.writeFileSync('dist/main.css', result.css);
if (result.sourceMap) {
  fs.writeFileSync('dist/main.css.map', JSON.stringify(result.sourceMap));
}

The key differences took me a moment to adjust to:

  1. The file path is the first argument - Not buried in an options object. Much cleaner.
  2. outputStyle is now style - Just a property rename.
  3. No outFile needed - The modern API doesn't write files for you. You get back the CSS and source map, then handle the file writing yourself.
  4. CSS is already a string - No more calling .toString() on a Buffer.
  5. Source maps are objects - Not strings, so you stringify them when writing.

Compiling strings instead of files

There are scenarios where you need to compile Sass without reading from a file. Build tools might construct Sass strings on-the-fly, or you could have a CMS that lets content editors define variables. Maybe you're generating a theme dynamically based on user input. In those cases, you're compiling strings directly:

Old way:

Code languagejavascript
const result = sass.renderSync({
  data: '$primary: #333; body { color: $primary; }',
  outputStyle: 'compressed',
});

New way:

Code languagejavascript
const result = sass.compileString('$primary: #333; body { color: $primary; }', {
  style: 'compressed',
});

Going async

One of the nice wins is async/await support. The old callback-based approach looked like this:

Code languagejavascript
sass.render(
  {
    file: 'styles/main.scss',
  },
  (err, result) => {
    if (err) {
      console.error(err);
      return;
    }
    console.log(result.css.toString());
  }
);

With the modern API:

Code languagejavascript
async function compileSass() {
  const result = await sass.compileAsync('styles/main.scss');
  console.log(result.css);
}

compileSass();

Much cleaner with async/await. If you're using this in a build tool or automation script, it's much more natural to work with.

Custom functions and importers

Custom functions let you extend Sass with your own logic. You might need a function to calculate responsive sizes, generate colour variants, or perform calculations that Sass's built-in functions don't handle. Importers let you override how Sass finds and loads files (useful if you're loading Sass from a database, a package manager, or a custom file system). These are more advanced use cases, but if you're doing either, the API changed significantly.

The old way with custom functions:

Code languagejavascript
const sass = require('sass');

const result = sass.renderSync({
  file: 'styles/main.scss',
  functions: {
    'double($n)': function (n, done) {
      done(sass.types.Number(n.getValue() * 2, n.getUnit()));
    },
  },
});

The modern approach:

Code languagejavascript
const sass = require('sass');

const result = sass.compile('styles/main.scss', {
  functions: {
    'double($n)': function (args) {
      const n = args[0].assertNumber('n');
      return new sass.SassNumber(n.value * 2, n.numeratorUnits);
    },
  },
});

The modern API uses a more robust system. It's less magical and more explicit, which honestly feels better.

What about build tools?

If you're using a build tool like webpack, Vite, or gulp, check whether they've updated their Sass integration. Most have, but support varies:

sass-loader (for webpack) supports the modern API in v14+ if you set api: 'modern-compiler'. That option gives you the performance bump I mentioned earlier.

Vite already uses the modern API by default, so if you're on a recent version, you're already getting the benefits.

gulp-sass is still working on full migration support, though it's improving.

If you're configuring sass-loader, the setup is straightforward:

Code languagejavascript
{
  loader: 'sass-loader',
  options: {
    api: 'modern-compiler',
    implementation: require('sass')
  }
}

That one option can make a noticeable difference in your build times if you've got a lot of Sass to process.

Should you update?

Yes.

Here's why.

Dart Sass 2.0.0 is coming, and the legacy API will be removed entirely. If you're still using renderSync() and render(), your build will break. There's no safety net, no deprecation option, no workaround.

Beyond that, the performance gains are real. If you're compiling Sass regularly as part of your workflow, cutting build times by 80-90% is worth the effort. The modern API is genuinely better to work with too.

If you do need time to migrate, you can suppress the warnings temporarily:

Code languagejavascript
sass.renderSync({
  file: 'styles/main.scss',
  silenceDeprecations: ['legacy-js-api'],
});

But don't rely on that, you'll be forced to update eventually anyway.

The lesson

I've only just noticed the warnings in my build logs recently. I could have accidentally spent years skipping it thinking “It works" was enough. Sometimes what works is just outdated, and you don't realise it until something forces you to look at it properly.

In this case, I didn’t know how much of a performance benefit I would be leaving on the table. I was using an API that had been superseded by something better, faster, and cleaner. All because I never upgraded a function call from (checks notes) 2015.

The renderSync() API served a purpose. It got Sass running in Node in the first place. But it's time to move on. The modern API isn't just a rename, it's a real improvement that makes your builds faster and your code cleaner.

So I've updated my build scripts. Compared to migrating from @import to @use and @forward, it didn't take long. The deprecation warning is gone and my builds are faster.

I probably should have done this when it came out five years ago, but at least I did it now.

If you're seeing that same deprecation warning in your logs, now is a good time to update yours too.

🦋 - likes

    🔄 - reposts

      Like this post on Bluesky

      Want to ensure your website meets accessibility standards and works for everyone?

      I'll audit and enhance your HTML, CSS, and JavaScript for WCAG compliance and inclusivity.

      get in touch!