suzaku-io / boopickle

Binary serialization library for efficient network communication

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Produce more compact JavaScript with Scala.JS

strelec opened this issue · comments

I have inspected the source code boopickle produces when compiling to JS. In general, it seemed very repetitive, and the thing that bothered me the most, is 450 occurrences of this exact piece of code:

h(x((new y).j((new v).d(["Cannot add same class (",") twice to a composite pickler"]))

170 occurences of this:

(new J).h("Deduplication is disabled, but identityFor was called."))
(new v).d(["Unknown object coding: ",""])

Also, overall, there are a lot of for-loops that get copy-pasted over and over. For almost no performance penalty, those could be extracted to a function.

This is due to heavy inlining performed by the Scala.js compiler. GZIPping the JS will compress those duplicates very well, so it's not a high cost for transfer.

This also depends on how you use BooPickle. If you just let the macro generate picklers whenever they are needed, you will end up with a lot of duplicate instances. If this becomes a problem, it's best to be more explicit where and when those picklers are generated.

Parsing and compiling the javascript in the browser is the thing that takes the most time - and what is worse - it takes this time in the critical user journey (time to interactive), while the page is loading.

Gzipping does not help with this problem. Parsing JS to AST and producing machine code could take more than download.

Video about JS parse costs ():
https://youtu.be/_srJ7eHS3IM

Moreover, I do not believe Scala.JS is doing 300 kilobytes of inlined strings. It is most probably the macros that you use.

It's a balance between runtime performance and load-time performance. Some of the things like the "deduplication" are marked as @inline in BooPickle code to make sure they get inlined in performance critical paths, whereas some others like the "composite pickler" are just inlined by Scala.js automatically. For the composite picklers the runtime performance is irrelevant since that code path is typically called only once, so it would make sense to mark them as @noinline to prevent inlining by the Scala.js optimizer.

If you'd like to measure the effect of removing the aforementioned parts, you could to a simple search&replace on the produiced JS and then look at Chrome dev tools if it makes any difference in the load/parse time.

As it happens, I have been doing similar benchmarking with the embedded ScalaFiddle recently, to minimize the time it takes to load embedded fiddles onto a web page :)

That is completely true, it is a tradeoff.

But the problem with most benchmarks is - they benchmark once the script is already loaded.

If you watched the video, the parse + compile stage for 1MB script can take 10 seconds on some (not even that old) mobile phones. Not inlining in case of Scala.JS seems like a really reasonable solution.

Yup, this is very true and an important point! If you can pinpoint where these excessive inlinings come from (in your code base), I can try to fix (most of) them for the next release. Some are probably nobrainers while others require some consideration regarding the impact on runtime perf.

I've pushed some changes to master so you can try it out locally by publishLocal the 1.2.7-SNAPSHOT version. Let me know how it looks on your code base!

Was trying to publish it locally for 2 hours, I always got the error that the version is not found, when building my own project. SBT is complicated.

I guess it's best to just publish it as a new point release.

Are you using Scala 2.11 or 2.12? The default publishLocal only publishes the 2.12 version, you need +publishLocal to publish both.

I have replaced the author from "me.chrons" to "io.suzaku" and publishLocal succeded.

Can confirm a huge reduction (200 KiB ~ 30%) in size before compression, and about 12 KiB after compression.

Good to hear! These improvements will then be in the next release, hopefully already this week.

Thanks @ochrons ! , the generated js goes from >15MB to 13.8MB for version 1.3.0