We use the GHC JS Backend (on GHC 9) to compile Haskell to JavaScript. We use the Reflex library for the UI.
Regarding deployment I was wondering how we can improve the compiled JS size. Because the all.js is a 15 MiB file and that is extremely large for the web.
I found resources for the old Ghcjs using the -dedupe flag, that seems to however not exist anymore.
What other flags and options exist to optimize our js file size? Are you aware of any resources for this?
Could some tools in the javascript ecosystem help or are they not suitable for this case? Have you tried it iut?
5 Likes
Yes, we are trying different approaches, which you can read here:
These links contain tools overview as well.
5 Likes
From what I understand these are mostly improvements in the compiler that I should automatically get when we upgrade.
We are currently on 9.10.0.20240413 due to an issue with the reflex-dom library on 9.12.1.
And the other option is using Google Closure Compiler, however this also need 9.12.1 as it runs into an error with the compiled JS of 9.10.0.20240413.
Is this correct? Or did I miss something in the links?
Roughly speaking yep, need try to use 9.12.2 at least, but you can give a chance for SWC
1 Like
From matrix:
> an issue with the reflex-dom library on 9.12.1.
No clue what that is
Don't recall seeing an issue either
Also from matrix
Using bun I can get it down from 10MB to 2MB
@panmona try the new 0.8.1.4 release
9.10 has much worse JS size output than 9.12 which itself is worse than 8.6 but better than 8.10
5 Likes
Thank you so much for fixing it so quickly! I’ll give it a shot.
@panmona After using 9.12 what is the final size which you reduced it too?
For later reference the mentioned issue was this one: Support for GHC JS 9.12.1 · Issue #496 · reflex-frp/reflex-dom · GitHub
How do you use bun (I assume it is this one: https://bun.sh) to decrease the file size?
I have upgrade our project to 9.12.1 and tried the following:
- This increased the file size to 99.7 MiB:
$ bun build --compile --minify all.js --outfile all.min.js
- This command failed with an error:
$ bun build --minify all.js --outfile all.min.js
9211 | var child_process = require('child_process');
^
error: Browser build cannot require() Node.js builtin: "child_process". To use Node.js builtins, set target to 'node' or 'bun'
Also I couldn’t figure out how you can use swc to minify? Do you need to use webpack for it?
Unfortunately it didn’t really reduce our file size by a lot. It is now 14.6 MiB instead of 17.6 MiB.
Additionally, Google Closure Compiler unfortunately still fails with an error, and I don’t know what I can do about it. It also happens without using Advanced compilation mode.
$ npx google-closure-compiler --js=all.js --js_output_file=out.min.js --compilation_level ADVANCED_OPTIMIZATIONS
all.js:357371:6: ERROR - [JSC_LANGUAGE_FEATURE] This language feature is only supported for UNSTABLE mode or better: Public class fields.
357371| name = 'ExitStatus';
^
1 error(s), 0 warning(s)
Take a look here. It clearly says that you need add –language_in UNSTABLE (or try ECMASCRIPT_NEXT if it will be enough).
Personally I have no experience with bun
and swc
but at first glance swc
is more mature.
Also take a look at the test testsuite/tests/javascript/closure/Makefile
. It may have useful options as well.
1 Like
Thanks. I didn’t see this. Unfortunately if I try this, it still gives a lot of errors.
I tried:
$ npx google-closure-compiler --js=all.js --js_output_file=out.min.js --compilation_level ADVANCED_OPTIMIZATIONS --language_in UNSTABLE
all.js:3540:11: WARNING - [JSC_REDECLARED_VARIABLE] Redeclared variable: d
3540| }else {var d=h$c3(h$pap_0,h$r1,((((h$r1.f.t===1)?h$r1.f.a:h$r1.d2.d1)-0)-1),null);
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
all.js:3557:11: WARNING - [JSC_REDECLARED_VARIABLE] Redeclared variable: h
3557| }else {var h=h$c3(h$pap_0,h$r1,((((h$r1.f.t===1)?h$r1.f.a:h$r1.d2.d1)-0)-1),null);
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-- many more
And I also tried it by adding the additional option: --externs all.externs.js
.
In which repo do you mean to look at the Makefile? There is no Makefile in the google-closure-compiler repo.
This seems to not be relevant for this case of minifying Javscript output of GHC.
The command in the miso package.json compiles self-written Javascript/Typescript: miso/ts at dfb831115e6f7f693e97d204744788046ae2b32a · dmjio/miso · GitHub
If I try the command like in miso the output mentions that we need to use node as a target:
$ bun build --outfile=all.min.js all.js --target=browser --minify
9211 | var child_process = require('child_process');
^
error: Browser build cannot require() Node.js builtin: "child_process". To use Node.js builtins, set target to 'node' or 'bun'
at /mypath/all.js:9211:35
The node target then works (=> bun build --outfile=all.min.js all.js --target=browser --minify
) and produces a 3.8 MiB file. However, the application then fails on startup with the error:
Uncaught SyntaxError: Cannot use import statement outside a module (at all.min.js:1:1)
Hmm, maybe you need cpp-options: -DGHCJS_BROWSER
to weed out node code. At least that was used in ghcjs days, not sure with js backend.
Doesn’t seem particularly documented for the new backend, but it at least appears in
Also the old ghcjs wiki might still have some relevant info: Deployment · ghcjs/ghcjs Wiki · GitHub though IIUC -dedupe
wasn’t ported (yet?).
What were actual errors?
Your message has warnings, they are fine.
--externs all.externs.js
is required
The repo.
1 Like
True. Sorry about that. These are the errors:
$ npx google-closure-compiler --js=all.js --externs all.externs.js --js_output_file=out.min.js --compilation_level ADVANCED_OPTIMIZATIONS --language_in UNSTABLE
... warnings ...
all.js:8831:11: ERROR - [JSC_UNDEFINED_VARIABLE] variable MK_INTEGER_S is undeclared
8831| return MK_INTEGER_S(parseInt(str, 10));
^^^^^^^^^^^^
all.js:8833:11: ERROR - [JSC_UNDEFINED_VARIABLE] variable h$ghcjsbn_readInteger is undeclared
8833| return h$ghcjsbn_readInteger(str);
^^^^^^^^^^^^^^^^^^^^^
all.js:8904:126: ERROR - [JSC_UNDEFINED_VARIABLE] variable a is undeclared
8904| while(--i>=0) r = (h$c2(h$ghczmprimZCGHCziTypesziZC_con_e, ((h$c1(h$ghczminternalZCGHCziInternalziJSziPrimziJSVal_con_e, (a[i])))), (r)));
^
all.js:8931:14: ERROR - [JSC_UNDEFINED_VARIABLE] variable ls is undeclared
8931| for(var i=ls-k;i>=0;i-=k) r = (h$c2(h$ghczmprimZCGHCziTypesziZC_con_e, ((h$c1(h$ghczminternalZCGHCziInternalziJSziPrimziJSVal_con_e, (x.substr(i,i+k))))), (r)));
^^
all.js:19980:0: ERROR - [JSC_UNDEFINED_VARIABLE] variable h$stg_absentErrorzh is undeclared
19980| h$stg_absentErrorzh(b,c);
^^^^^^^^^^^^^^^^^^^
all.js:19986:0: ERROR - [JSC_UNDEFINED_VARIABLE] variable h$stg_paniczh is undeclared
19986| h$stg_paniczh(h$ghczmprimZCGHCziPrimziPaniczilvl_1,h$ghczmprimZCGHCziPrimziPaniczilvl_2);
^^^^^^^^^^^^^
6 error(s), 80 warning(s)
UPDATE:
It seems like I have gotten it to work now thanks to the old wiki that @alexfmpe linked.
I used the additional option --jscomp_off=checkVars
and then it completed successfully.
So my command was: $ npx google-closure-compiler --js=all.js --jscomp_off=checkVars --externs all.externs.js --js_output_file=out.min.js --compilation_level ADVANCED_OPTIMIZATIONS --language_in UNSTABLE
The file is now 3.3 MiB. And from my initial testing everything seems to still work as expected!
But I assume I should also use the following two additional options: --isolation_mode=IIFE --assume_function_wrapper
as documented in the old wiki and its also used in the Makefile mentioned before.
1 Like
Hmm. That seems like a reasonable assumption!
I am either doing something wrong or this does not work for the new backend?
I added cpp-options: -DGHCJS_BROWSER
to the part of the cabal file where my executable is listed and then when I try to build it, I get this:
emcc: error: DGHCJS_BROWSER: No such file or directory ("DGHCJS_BROWSER" was expected to be an input file, based on the commandline arguments provided)
dist-newstyle/build/javascript-ghcjs/ghc-9.12.1/appname-0.1.0.0/x/appname-exe/build/appname-exe/autogen/Paths_appname.hs:1:1: error:
`emcc' failed in phase `Haskell C pre-processor'. (Exit code: 1)
|
1 | {-# LANGUAGE CPP #-}
| ^
Error: [Cabal-7125]
Huh, good question.
Back with ghcjs both miso and reflex-dom used cpp-options
but maybe that’s different for js backend?