node-sass icon indicating copy to clipboard operation
node-sass copied to clipboard

Compile to WebAssembly

Open mafintosh opened this issue 8 years ago • 46 comments

Since Node 8 supports WebAssembly it would be nice to compile libsass to that using emscripten so people wouldn't need to compile the c++ or depend on a prebuild.

mafintosh avatar Jun 09 '17 23:06 mafintosh

Definitely open to it. We're also about to give napi a shot also. It's an exciting time for native extensions.

xzyfer avatar Jun 10 '17 01:06 xzyfer

There already is an official libsass Emscripten project https://github.com/medialize/sass.js

nschonni avatar Jun 10 '17 01:06 nschonni

Yeah but with wasm we can in theory entirely ditch vendor binaries for Node 8+

On 10 Jun. 2017 11:54 am, "Nick Schonning" [email protected] wrote:

There already is an official libsass Emscripten project https://github.com/medialize/sass.js

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/sass/node-sass/issues/2011#issuecomment-307534639, or mute the thread https://github.com/notifications/unsubscribe-auth/AAjZWCpQgdjK6yHR0HJ41REgUVvD7Azvks5sCfdBgaJpZM4N16hz .

xzyfer avatar Jun 10 '17 02:06 xzyfer

Dropping binaries would be fantastic... I wonder how well will that setup perform

saper avatar Jun 10 '17 19:06 saper

@mafintosh I'm starting look into this seriously, but I'm struggling with finding resources on how to do this for native extensions in Node. Do you know of any other projects that have made this switch to WASM that I can reference?

xzyfer avatar Aug 04 '17 06:08 xzyfer

@xzyfer I tried to install the Emscripten SDK on my computer to compile libsass to WASM; good news, it compiles without changing the C++ code!

I follow the following steps:

  1. Activate the Emscripten SDK on my environment
  2. On the latest commit of libsass, I run the makefile with the Emscripten compiler:
$ make clean && make --eval="CC=emcc
> CXX=emcc"
  1. Then I compile the library into wasm: emcc src/*.o lib/libsass.a -O2 -s WASM=1 -o libsass.js

The libsass.wasm file should now contain the library, and can be run by node >=8 Also I try to change the binding.js file of node-sass to use the cross-platform WASM instead of the platform-dependent binary, but I suppose I'm missing something since node displays an error:

In binding.js, I changed:

return ext.getBinaryPath()

by

    return require('./libsass.js'); // libsass.js is the file generated by emcc

And it outputs the following:

TypeError: binding.libsassVersion is not a function
    at Object.getVersionInfo (.../node_modules/node-sass/lib/extensions.js:368:27)
    at Object.<anonymous> (.../node_modules/node-sass/lib/index.js:448:28)
    at Module._compile (module.js:573:30)
    at Object.Module._extensions..js (module.js:584:10)
    at Module.load (module.js:507:32)
    at tryModuleLoad (module.js:470:12)
    at Function.Module._load (module.js:462:3)
    at Module.require (module.js:517:17)
    at require (internal/module.js:11:18)
    at Object.<anonymous>

I hope I'm helping!

aduh95 avatar Sep 03 '17 13:09 aduh95

Thanks for this work @aduh95. You got further than I did when I looked at it. For now I would comment out the binding.libsassVersion code paths to continue making progress.

xzyfer avatar Sep 04 '17 10:09 xzyfer

for anyone interested in this topic, I've prepped small poc at https://github.com/sass/node-sass/pull/2220 for further discussions.

kwonoj avatar Jan 19 '18 06:01 kwonoj

Thank you! I've recently got sassc running as web assembly, but haven't had much luck with node-gyp. I'll take a closer look this weekend.

On 19 Jan. 2018 5:41 pm, "OJ Kwon" [email protected] wrote:

for anyone interested in this topic, I've prepped small poc at #2220 https://github.com/sass/node-sass/pull/2220 for further discussions.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/sass/node-sass/issues/2011#issuecomment-358880372, or mute the thread https://github.com/notifications/unsubscribe-auth/AAjZWHsXziurKG7LXrcgNOfsR2shmt-Xks5tMDkFgaJpZM4N16hz .

xzyfer avatar Jan 19 '18 06:01 xzyfer

For anyone who's interested in this further, I spawned https://github.com/kwonoj/libsass-asm based on my PR #2220 to push this further. So far it passes sass-spec for 3.5.4.

kwonoj avatar Jul 22 '18 03:07 kwonoj

I came here after watching @callahad's amazing talk about Webassembly where he mentioned this project's potential for Webassembly.

Thought this might be of interest to the people who are currently putting time in this!

gijsroge avatar Oct 23 '18 16:10 gijsroge

Is there a plan to use WebAssembly (libsass-asm) as a replacement for libsass directly? As in, is it being seriously considered? Or are we still in the POC phase?

I periodically run into issues with node-sass/libsass, particularly when switching Node versions because the binaries don't match up. But also, I'd like to remove the network dependency from my toolchain, and currently downloading libsass is the only thing I haven't been able to avoid.

Understand that everyone is busy - feels like we're on the edge of some sort of utopia... it's very exciting (and I can't wait!).

justrhysism avatar Nov 22 '18 06:11 justrhysism

Compiling LibSass to WASM is easy. The difficulty is in compiling the C++ code that binds LibSass to Node. This is difficult in part because we need to rely on libuv for async custom importers, and the async render function. This is pretty much where we are stuck right now. The situation may improve when some kind of background worker or threading API lands in WebAssembly.

xzyfer avatar Nov 22 '18 06:11 xzyfer

Reading through this PR it appears that threading is available behind an experimental flag in Node 10.

Obviously there’s risk that might change - but is that enough to “unstuck” you? Is it even what you actually need?

Really wishing I could C and help. Might be time to level up...

Thank you for your prompt response @xzyfer :)

justrhysism avatar Nov 22 '18 10:11 justrhysism

Compiling LibSass to WASM is easy. The difficulty is in compiling the C++ code that binds LibSass to Node.

Isn't there a way to use libsass only as a self-contained library without any other bindings to eg. file io? We could extract the core functionality into a libsass-core or something, and make libsass just a wrapper over libsass-core. That way we could make it use callback-style async file io and map that to nodes fs api, so it wouldn't require any direct libsass <=> node magic. We could make this even work in the browser eg as a webpack runtime loader or on a scss to css conversion website.

Really wishing I could C and help. Might be time to level up...

i "leveled up" to rust, so im now even less willing to learn/"level down" to c :(

chpio avatar Jan 09 '19 13:01 chpio

FWIW, the folks of sass.js have an experimental WASM branch (which they apparently don't actively operate on) which works pretty nicely — I've at least tested it successfully in browsers:

medialize/sass.js#feature/wasm

loilo avatar Mar 13 '19 10:03 loilo

There is also this:

https://github.com/kwonoj/docker-libsass-wasm https://github.com/kwonoj/libsass-asm

glebm avatar Apr 15 '19 06:04 glebm

So the main issue is with how to call asynchronous JS functions (importers, sass functions) from libsass. I see two possible ways to do it:

a) Run libsass in a Worker via worker_threads (only on Node 10+, and behind a flag in 10.*) and use SharedArrayBuffer + Atomics to implement a lock, allowing the worker to synchronously wait for the importer. That "hard part" is already implemented in https://github.com/hyperdivision/async-wasm. I guess @mafintosh already knows about that project, since he's the author. ;)

b) Modify libsass to avoid the need for async - make it somehow possible to suspend the libsass code and resume it when the importer is done. This sounds difficult, but the sass.js code linked by @loilo earlier actually already does it! It happens automatically thanks to Emscripten. They use https://github.com/emscripten-core/emscripten/wiki/Emterpreter and emterpreter_whitelist.json. I think this looks pretty promising, but one possible problem is that sass.js only supports custom importers and not custom functions. Adding custom functions might make that whitelist much longer and force most of libsass code to the emterpreter "slow path". So it would need some benchmarking to find out how much slower is it.

TomiBelan avatar Jun 12 '19 11:06 TomiBelan

Good news! I did it! :tada:

I have a working "node-sass-wasm" which supports the full node-sass API, including asynchronous importers and functions (thanks to worker_threads), and passes all of test/api.js and test/spec.js. There are no more binaries to download, it has zero dependencies, and it doesn't need rebuilding for every future major version of Node. After some remaining minor cleanups, I'll put it on my GitHub so that everyone can take a look and try it. Stay tuned.

Bad news! It's a total rewrite! :broken_heart:

It basically doesn't share any code with node-sass. Especially not the C++ part - it's all done with Emscripten and Embind instead of V8 API and Nan. (More details incoming once I publish the code.)

I'd really like to contribute this code to node-sass so everyone can benefit from it, but I don't have any idea how. Node-sass still needs to support old Node versions and it's not clear to me how to combine them - two parallel codebases sound bad... Node-sass maintainers, what do you think?

TomiBelan avatar Jun 18 '19 15:06 TomiBelan

Wow, congratulations! Have you also rewritten libsass or has this been handled with emscripten? What is the node oldest version it can run on?

saper avatar Jun 18 '19 15:06 saper

I'm curious to see the implementation.

Update from our end: there's work going on right now to enable WASI un LibSass as well as libuv. This is the work we've been waiting for in order to ship node-sass as WASM.

We've investigate emscripten before but the resulting WASM module was large, and the performance suffered significantly.

xzyfer avatar Jun 18 '19 15:06 xzyfer

I'll try to publish the code ASAP so you can take a look. Sorry, it's not quite ready yet.

I didn't rewrite libsass. I'm compiling libsass with Emscripten and wrapping it with Embind, with a bit of special sauce to handle async importers/functions. It can run on node >= 10.5.0 with the --experimental-worker flag, and on node >= 11.7.0 without the flag.

The wasm file is 1.8 MB, which is not a lot. (The binding.node file in node-sass alone is 3.8 MB, and with all dependencies npm i node-sass; du -bsh node_modules is 18 MB.) Runtime performance seems decent, I'll get more data. Although I did notice that the startup time is quite slow: loading the wasm (during the first call to render / renderSync) can take ~0.9 s. :/

TomiBelan avatar Jun 18 '19 15:06 TomiBelan

Node-sass still needs to support old Node versions and it's not clear to me how to combine them - two parallel codebases sound bad... Node-sass maintainers, what do you think?

Could do a @next or major version bump which supports Node 10 or above? Doesn't make much sense to keep 2 parallel codebases just for Node 8 (i'm assuming your work supports Node 10+)

jasonwilliams avatar Jun 18 '19 15:06 jasonwilliams

From our point of view we've been waiting for the ecosystem and WASM to catch up. The reality is it's not ready our use case. Which isn't to say it's not possible, but the results thus far have negated the benefits of node-sass over other native JS implementations.

Supporting the full node-sass API is a very impressive effort and I'm excited to see it. But if we can't overcome the start up or run performance issues we're unlikely to progress down this path. Alternative pure JS implementations exist at the cost slower compilation speeds.

We'll continue to invest in improving the installation process as much as npm and node allow us to. And there is on going work in LibSass to reduce the memory footprint and further improve Sass compilation times - widening the gap between pure JS implementations.

xzyfer avatar Jun 18 '19 16:06 xzyfer

@xzyfer are these times measured somewhere? (the pure JS compilation and the C++ compilation)

jasonwilliams avatar Jun 18 '19 16:06 jasonwilliams

It's worth noting that folks working on WASM and WASI are seriously looking specifically at the issues faced by node-sass and LibSass as part of their work to evolve the specs.

xzyfer avatar Jun 18 '19 16:06 xzyfer

@jasonwilliams there have been various benchmarks over the years. You'll want to pay close attention to the language used in these benchmarks. I suggest looking at the raw numbers if they exist.

Some that I'm aware of are:

  • https://github.com/postcss/benchmark/blob/master/README.md
  • https://github.com/sass/dart-sass/blob/master/perf.md

xzyfer avatar Jun 18 '19 16:06 xzyfer

Most of these are out of date now, and non of them include the recent changes on LibSass master (unreleased).

xzyfer avatar Jun 18 '19 16:06 xzyfer

The prototype is ready. :tada: Take a look: https://github.com/TomiBelan/node-sass-wasm

A very unprofessional benchmark says test/api.js + test/spec.js runs in 14.4 seconds on node-sass but in 25.6 seconds on node-sass-wasm. So it's not only the startup time, the execution is also ~2x slower. This is worrying to me, because most of these tests are sass-spec tests which shouldn't spend a lot of time in the binding layer or Emscripten system code. Is this performance penalty intrinsic to WebAssembly itself? It might also be interesting to compare it vs sass.js and @kwonoj's libsass-asm.

Anyway, @xzyfer I think I agree: it's best for node-sass to keep using the native library for now. I hope node-sass-wasm can serve as inspiration but currently it's not realistic to merge it - especially because of (un)supported Node versions, but also because of performance. So I won't send it as a PR, but of course feel free to reuse any pieces that look useful (in line with MIT).

Still, I'll put node-sass-wasm on npm, as some users might find it to be a good middle ground between node-sass and native JS implementations. Should I make it a scoped package like @TomiBelan/node-sass-wasm to avoid name-squatting or sounding too "official"?

I'm also curious about your WASI comments, do you have any links? I thought WASI is just a libc-like / POSIX-like "system interface" which didn't sound terribly relevant. Does WASI solve the ABI problem, e.g. converting std::string <-> JS String, and more complex structures? That was the main reason to use Emscripten for me.

TomiBelan avatar Jun 22 '19 00:06 TomiBelan