Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(index): add noImporter option (options.noImporter) #307

Closed
wants to merge 1 commit into from

Conversation

anchann
Copy link

@anchann anchann commented Nov 22, 2016

Function calls associated with the advanced file resolution described above can add up to a significant time on projects with a large number of scss files. On a project with some 130 components each requiring an .scss file, sass build took 240 seconds with the importer, and 3 seconds without.

Everything below is my attempt at keyword bait for the next unfortunate soul dealing with this issue, as it took me half a day to figure out which part of the webpack pipeline is responsible for the slowness.

webpack 2, css-loader, sass-loader, style-loader, ExtractTextPlugin, extract-text-webpack-plugin, slow large project, many imports, require .sass from .js, super-slow, really slow, insanely slow

@jhnns
Copy link
Member

jhnns commented Nov 22, 2016

Thx for your feedback @anchann

I'm considering this change, although I don't think it's right to pull this in. We should rather try to tackle the perf issues.

Unfortunately, I can confirm that a lot of time can be spend on resolving. I've published a resolve-perf-tests that uses process.hrtime() and console.log()s to get some numbers and it seems like the sass-loader is spending 35% up to 73% of the time just to resolve files. This is unacceptable and should be solved at webpack level. However, I tried to reproduce these numbers in a bigger project and I couldn't. So, that remains a little vague. Maybe it's related to sass' complicated resolving algorithm involving underscore file names (_blahblah)

240 seconds to 3 seconds are extreme numbers. Can you give me some hints on your directory structure and how you import stuff?

@anchann
Copy link
Author

anchann commented Nov 23, 2016

Thanks for taking a look, Johannes.

we should rather try to tackle the perf issues

Fair enough. The only thing I wanted was that if somebody faces this problem, my findings are shared and indexed in google. The last few weeks of working on converting a large project from a three-year-old grunt-based toolchain to a webpack one has taught me one thing — none of this stuff works like magic out of the box, at the end of the day, you have to dig inside node_modules to get it working. It was the same story with the typescript part of the toolchain — takes forever, no output to shed light onto what's happening, yet calling tsc on the command line builds the project in seconds. I'd love to be proven wrong, but until then, logging my findings for the next dev is the best I can hope for :)

should be resolved at the webpack level

My personal opinion (which, I realize, is worth little) is that too much magic is a bad thing. Sass has since inception supported an includePaths configuration option specifically to make imports more readable and flexible. From this perspective, webpack's resolution seems to add nothing in terms of functionality, though perhaps it does add something in terms of uniformity (that said, uniform opaqueness is not necessarily a good thing). I'm actually using importPaths specifically to be able to import scss files from other node_modules, having it set to ["src/styles", "node_modules", "node_modules/compass-mixins/lib"]

Also, importers are labelled as experimental in node-sass' documentation. Would it not make sense to make the use of an experimental feature optional, specifically because the upstream library labels it as a "use with caution" feature? I would personally go as far as making it disabled by default, and only enabled when a user knows that they explicitly want the feature.

maybe it's related to sass' complicated resolving algorithm involving underscore file names

I don't think that's the case — disabling the webpack importer leaves sass' native underscore resolution intact, does it not?

directory structure

When debugging, I had a single .scss file that was importing 130 or so individual components' .scss files (since then I've switched back to require'ing .scss files at the top of each react component's .ts file). Each of these would in turn import a common_definitions.scss file, which in turn imported 4 leaf files with a bunch of variable or mixin declarations, along with a my-lib/styles/export_definitions.scss file, which imports 3 leaf files, along with

compass
compass/css3
compass/css3/images

These are provided by the compass-mixins npm module, as a replacement for the original ruby-based compass library that I was originally using before starting the migration to webpack.

The compass directory has on the order of 50 files in it. So all in we're looking at around 60 imports per file, which presumably means 60 resolve calls for 130 files, or around 8000 calls. That would make it something like 300 ms per call, which I'm pretty sure isn't what's happening. Yesterday when debugging I was dumping timings for resolution calls, and most were in the low tens of milliseconds, though about one in twenty clocked in at low hundreds.

Let me know if this is enough information for you to go off of. If not, and you think it's worth the time, I'll hack the loader to add some timers to give a more concrete picture. Or, if it's easier for you, you can give me a custom version of sass-loader with whatever instrumentation you want, and I'll send you the output data.

@jhnns
Copy link
Member

jhnns commented Nov 23, 2016

takes forever, no output to shed light onto what's happening, yet calling tsc on the command line builds the project in seconds. I'd love to be proven wrong, but until then, logging my findings for the next dev is the best I can hope for :)

We already heard of builds taking minutes to finish. Unfortunately, we don't have complex example projects to replicate these issues. My projects usually finish after max. 20 seconds in production mode, but they're medium sized.

webpack's resolution seems to add nothing in terms of functionality, though perhaps it does add something in terms of uniformity (that said, uniform opaqueness is not necessarily a good thing)

Since webpack provides its own resolving algorithm, I felt that it would be consistent for the developer to configure all resolving stuff in your webpack.config. It's true that this uniformity is not acceptable at the current performance cost, so I understand your argument here.

Also, importers are labelled as experimental in node-sass' documentation. Would it not make sense to make the use of an experimental feature optional, specifically because the upstream library labels it as a "use with caution" feature? I would personally go as far as making it disabled by default, and only enabled when a user knows that they explicitly want the feature.

Our importer is well-tested and works without errors, so I don't see the need to declare this as an experimental thing.

I don't think that's the case — disabling the webpack importer leaves sass' native underscore resolution intact, does it not?

Once we use our own importer, sass' native importing algorithm is not available. That's why we have to replicate it (and it's unnecessary complex 😢)

Or, if it's easier for you, you can give me a custom version of sass-loader with whatever instrumentation you want, and I'll send you the output data.

Could you use the sass-loader from the resolve-perf-tests branch and tell me the last output (it's accumulating all numbers)?

@jhnns
Copy link
Member

jhnns commented Dec 26, 2016

Could you check if the import is still slow when you change UV_THREADPOOL_SIZE to a big number (like 200)?

Just set the environment variable before calling webpack.

@anchann
Copy link
Author

anchann commented Dec 27, 2016

Apologies for the silence: after I finally finished the conversion work on my project, the pressure to work on user-facing features out-prioritized my feeble attempts at community involvement.

To be honest, I didn't even want to do this test when I saw the variable name, since it sounded like a poor quality sledgehammer approach to the problem. I still firmly believe that even if resolving the problem internally inside webpack is the philosophically correct thing to do, given that lib-sass has a working solution to accommodate aliasing via import paths, sass-loader should provide its users with an escape hatch in case they want one.

Either way, I refactored my project a bit to get back to a setup about which I originally posted, one with a top level scss file that imports 133 component-specific scss files. There were some library-specific files that I didn't bother with, so they'll show up stand-alone in the output. I do not know why webpack builds the whole thing twice — I didn't even know that it does before I put the instrumentation in. Instrumentation is basically the following timer:

269     var startTime = new Date().getTime();
270     asyncSassJobQueue.push(sassOptions, function onRender(err, result) {
271         console.log("Took " + (new Date().getTime() - startTime) + " ms for " + resourcePath);

Here's what the numbers look like (resourcePath stripped for ease of reading/privacy).

First, with the importer disabled (which is how I run it normally).

Took 466 ms for lib/components/ImageOrPlaceholder/ImageOrPlaceholder.scss
Took 257 ms for lib/components/RaisedButton/RaisedButton.scss
Took 638 ms for lib/components/CircularProgress/CircularProgress.scss
Took 867 ms for lib/components/IconButton/IconButton.scss
Took 1187 ms for lib/components/TapCapture/TapCapture.scss
Took 1210 ms for lib/components/FlatButton/FlatButton.scss
Took 1355 ms for lib/components/Spinner/Spinner.scss
Took 1439 ms for lib/components/TextField/TextField.scss
Took 1586 ms for lib/components/ListItem/ListItem.scss
Took 4532 ms for app/src/styles/top-level-everything-sass-loader-test.scss
Took 3095 ms for lib/components/Photos/Photos.scss
Took 71 ms for lib/components/ImageOrPlaceholder/ImageOrPlaceholder.scss
Took 125 ms for lib/components/CircularProgress/CircularProgress.scss
Took 146 ms for lib/components/RaisedButton/RaisedButton.scss
Took 188 ms for lib/components/IconButton/IconButton.scss
Took 221 ms for lib/components/Spinner/Spinner.scss
Took 263 ms for lib/components/TapCapture/TapCapture.scss
Took 288 ms for lib/components/FlatButton/FlatButton.scss
Took 339 ms for lib/components/TextField/TextField.scss
Took 379 ms for lib/components/Photos/Photos.scss
Took 411 ms for lib/components/ListItem/ListItem.scss
Took 1518 ms for app/src/styles/top-level-everything-sass-loader-test.scss

Next, with the latest sass-loader from npm. Even though the calls go through sass-loaders async functions, it looked as if something was serializing the calls along the way. Upon closer inspection, this seems to also be happening in the no-importer version, but because the times are lower, it's less obvious. CPU was sitting at around 95% (out of a total of 400% available) the entire time.

Took 430 ms for lib/components/ImageOrPlaceholder/ImageOrPlaceholder.scss
Took 5918 ms for lib/components/Spinner/Spinner.scss
Took 9719 ms for lib/components/CircularProgress/CircularProgress.scss
Took 9735 ms for lib/components/RaisedButton/RaisedButton.scss
Took 12976 ms for lib/components/IconButton/IconButton.scss
Took 13081 ms for lib/components/TapCapture/TapCapture.scss
Took 16249 ms for lib/components/FlatButton/FlatButton.scss
Took 16273 ms for lib/components/TextField/TextField.scss
Took 20051 ms for lib/components/ListItem/ListItem.scss
Took 19202 ms for lib/components/Photos/Photos.scss
Took 270455 ms for app/src/styles/top-level-everything-sass-loader-test.scss
Took 3999 ms for lib/components/CircularProgress/CircularProgress.scss
Took 4022 ms for lib/components/ImageOrPlaceholder/ImageOrPlaceholder.scss
Took 8159 ms for lib/components/FlatButton/FlatButton.scss
Took 8193 ms for lib/components/RaisedButton/RaisedButton.scss
Took 11526 ms for lib/components/Spinner/Spinner.scss
Took 11545 ms for lib/components/IconButton/IconButton.scss
Took 15196 ms for lib/components/ListItem/ListItem.scss
Took 15216 ms for lib/components/TapCapture/TapCapture.scss
Took 19013 ms for lib/components/Photos/Photos.scss
Took 19015 ms for lib/components/TextField/TextField.scss
Took 233537 ms for app/src/styles/top-level-everything-sass-loader-test.scss

Finally, with the UV_THREADPOOL_SIZE set to 200, we do see a bunch of parallelization happening with the smaller files, but the big top level index of all components' scss still takes about the same amount of time.

Took 290 ms for lib/components/ImageOrPlaceholder/ImageOrPlaceholder.scss
Took 13724 ms for lib/components/Spinner/Spinner.scss
Took 13752 ms for lib/components/FlatButton/FlatButton.scss
Took 13798 ms for lib/components/IconButton/IconButton.scss
Took 13975 ms for lib/components/TextField/TextField.scss
Took 14344 ms for lib/components/CircularProgress/CircularProgress.scss
Took 14343 ms for lib/components/TapCapture/TapCapture.scss
Took 13347 ms for lib/components/Photos/Photos.scss
Took 14380 ms for lib/components/ListItem/ListItem.scss
Took 16861 ms for lib/components/RaisedButton/RaisedButton.scss
Took 268432 ms for app/src/styles/top-level-everything-sass-loader-test.scss
Took 26 ms for lib/components/ImageOrPlaceholder/ImageOrPlaceholder.scss
Took 11642 ms for lib/components/TextField/TextField.scss
Took 11673 ms for lib/components/CircularProgress/CircularProgress.scss
Took 11715 ms for lib/components/Spinner/Spinner.scss
Took 11920 ms for lib/components/IconButton/IconButton.scss
Took 11950 ms for lib/components/ListItem/ListItem.scss
Took 12097 ms for lib/components/TapCapture/TapCapture.scss
Took 12112 ms for lib/components/FlatButton/FlatButton.scss
Took 12144 ms for lib/components/Photos/Photos.scss
Took 14632 ms for lib/components/RaisedButton/RaisedButton.scss
Took 245477 ms for app/src/styles/top-level-everything-sass-loader-test.scss

@jhnns
Copy link
Member

jhnns commented Dec 29, 2016

Thx for your efforts. This helps me a lot! 👍

To be honest, I didn't even want to do this test when I saw the variable name, since it sounded like a poor quality sledgehammer approach to the problem.

No worries! This won't be the final solution. I just wanted to check if it might be a problem that node-sass eats up almost all available threads for file IO. There was a problem in libsass that caused a deadlock (#100 #147) which was the reason why we had to use async's job queue. This way, node-sass still leaves one thread available for node.js – which is also the only thread for webpack's file IO. Thus webpack can only resolve files sequentially. And I think your numbers confirm my assumption. For instance, take a look at RaisedButton.scss. In your original setup, it only takes 200ms. But with all the file IO going on it is delayed up to 9000ms.

I still firmly believe that even if resolving the problem internally inside webpack is the philosophically correct thing to do, given that lib-sass has a working solution to accommodate aliasing via import paths, sass-loader should provide its users with an escape hatch in case they want one.

webpack's resolving engine is more powerful than node-sass' includePaths. For instance, you can also alias paths (you can't do that with node-sass). And hopefully with webpack 2 you will also be able to @import JS files in order to translate JS variables into Sass variables. But I agree that for most use-cases, node-sass' includePaths is totally sufficient.

My current approach is:

  1. Try to understand why we are currently slower
  2. Try to find a simple solution to make webpack almost as fast as node-sass. It will always be a little bit slower because there is more going on behind the scenes. But it should not make a big difference
  3. If I can find a simple and fast solution, I will provide a flag to disable webpack's resolving engine. I don't want to add too much options because it makes everything more complicated. But if we can't make sass-loader faster, we should definitely allow that option.

@anchann
Copy link
Author

anchann commented Dec 29, 2016

webpack's resolving engine is more powerful than node-sass' includePaths. For instance, you can also alias paths

You can make an aliases directory, symlink whatever you need into there, and add it to includePaths. Works great.

There is also another good reason to use each technology's native features, rather than something like webpack's resolver, that I recently ran into with TypeScript. There are various text editors that support things like code completion, display errors as you edit, let you jump to definition, etc. These tools typically don't know how to read webpack's config and tease out alias information, so as far as they are concerned, the import/require statements are broken and point to non-existing files. Much like sass, TypeScript happens to have its own system for managing aliases/includes, through a tsconf.json file, which most editors understand. So while webpack's goal of providing a resolving engine that works across the whole stack is admirable, it breaks other tooling.

I think your argument would convince me in the case if webpack just worked, out of the box. But if for advanced use cases you need to dig into the code to get things working, it doesn't matter whether webpack provides advanced functionality, or whether you hack it yourself with symlinks or something — you spend the same amount of time. Now, I understand that your efforts are actually geared specifically at getting webpack to the point where everything just works out of the box. But I personally believe that this goal is impossible to achieve, because new versions of various technologies come out at about the same speed as it takes to polish off an out-of-the-box-working toolchain to support them. So by the time everything works great with webpack, there will be some new kid on the block, also as cool and must-switch-over-to, and as not-working-out-of-the-box for advanced use cases. This probably sounds cynical, but I've seen it happen enough times now to see a pattern. Though who knows, maybe we'll reach some kind of equilibrium point once a toolchain that satisfies most common use cases emerges.

Anyway, sorry for rambling. It's a month since I finished the stack transition, and I got my momentum back, so now I can afford a few cycles here and there to at least give you metrics if you need them. So let me know if you need to test anything against my setup, whether the artificial one-big-file-with-many-imports, or the actual one that I use, where each components' .scss file is require'd at the top of the .ts file.

@jhnns
Copy link
Member

jhnns commented Mar 22, 2017

I did some profiling: #296 (comment)

@clydin
Copy link

clydin commented May 9, 2017

Regardless of the performance concerns, there is definite user need to allow standard sass resolution semantics instead of webpack specific resolution.
The feature I would prefer however would be more of a "webpackResolution" Boolean setting that would not add the webpack importer but still allow custom importers.

@alexander-akait
Copy link
Member

@anchann friendly ping, what is status?

@anchann
Copy link
Author

anchann commented Jun 15, 2017

@evilebottnawi Is something blocked on me? Ever since I originally posted the question, I've been using my own hacked version with the webpack resolution disabled.

@joshwiens
Copy link
Member

Given this is performance related, @jhnns has the final say as to how & when this gets merged.

@michael-ciniawsky michael-ciniawsky changed the title Advanced import resolution makes builds very slow on larger projects feat: add noImporter option (options.noImporter) Feb 28, 2018
Copy link
Member

@michael-ciniawsky michael-ciniawsky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-1 I don't think this is a sufficient solution to any perf related problem and adds more usage confusion. There needs to be another solution found to solve this imho and if there isn't one yet, the perf hit needs to be simply taken in the meantime

@michael-ciniawsky michael-ciniawsky changed the title feat: add noImporter option (options.noImporter) feat(index): add noImporter option (options.noImporter) Feb 28, 2018
@michael-ciniawsky
Copy link
Member

In case no one objects within the next ~48 hours, I'm going ahead and declining this PR for now

@alexander-akait
Copy link
Member

Closing due to inactivity. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants