Fable icon indicating copy to clipboard operation
Fable copied to clipboard

Fable producing very large files

Open davidtme opened this issue 3 years ago • 7 comments

Description

We have a record with around 200+ fields which is getting updated quite a lot using an Elmish page. However because of the number of field and updates the field size has grown to over 4 MB.

But this can also be the case with 30 fields with more updates.

Unfortunately webpack doesn't really do much to reduce the size.

Repro code

I have mocked up a small code snippet that balloons into a 2.18 MB file which I think is around 217 times bigger.

type SomeRecord =
    { FieldWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName1 : int
      FieldWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName2 : int
      FieldWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName3 : int
      FieldWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName4 : int
      FieldWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName5 : int
      FieldWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName6 : int
      FieldWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName7 : int
      FieldWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName8 : int
      FieldWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName9 : int
      FieldWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName10 : int
      FieldWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName11 : int
      FieldWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName12 : int
      FieldWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName13 : int
      FieldWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName14 : int
      FieldWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName15 : int
      FieldWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName16 : int
      FieldWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName17 : int
      FieldWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName18 : int 
      FieldWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName19 : int 
      FieldWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName20 : int      
      FieldWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName21 : int
      FieldWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName22 : int
      FieldWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName23 : int
      FieldWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName24 : int
      FieldWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName25 : int
      FieldWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName26 : int
      FieldWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName27 : int
      FieldWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName28 : int 
      FieldWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName29 : int 
      FieldWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName30 : int   }

// Some functions that force lots of updates the the record
let inline updateTheRecord1 (item : SomeRecord) = 
    { item with FieldWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName1 = 1 }

let inline updateTheRecord2 (item : SomeRecord) = 
    item |> updateTheRecord1 |> updateTheRecord1 |> updateTheRecord1 |> updateTheRecord1 |> updateTheRecord1 |> updateTheRecord1 |> updateTheRecord1 |> updateTheRecord1 |> updateTheRecord1 |> updateTheRecord1

let inline updateTheRecord3 (item : SomeRecord) = 
    item |> updateTheRecord2 |> updateTheRecord2 |> updateTheRecord2 |> updateTheRecord2 |> updateTheRecord2 |> updateTheRecord2 |> updateTheRecord2 |> updateTheRecord2 |> updateTheRecord2 |> updateTheRecord2 

let updateTheRecord4 (item : SomeRecord) = 
    item |> updateTheRecord3 |> updateTheRecord3 |> updateTheRecord3 |> updateTheRecord3 |> updateTheRecord3  // any more than this an you get a stack error when running the repl
    
let item = 
    { FieldWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName1 = 0
      FieldWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName2 = 0
      FieldWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName3 = 0
      FieldWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName4 = 0
      FieldWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName5 = 0
      FieldWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName6 = 0
      FieldWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName7 = 0
      FieldWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName8 = 0
      FieldWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName9 = 0
      FieldWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName10 = 0
      FieldWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName11 = 0
      FieldWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName12 = 0
      FieldWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName13 = 0
      FieldWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName14 = 0
      FieldWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName15 = 0
      FieldWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName16 = 0
      FieldWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName17 = 0
      FieldWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName18 = 0
      FieldWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName19 = 0
      FieldWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName20 = 0
      FieldWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName21 = 0
      FieldWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName22 = 0
      FieldWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName23 = 0
      FieldWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName24 = 0
      FieldWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName25 = 0
      FieldWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName26 = 0
      FieldWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName27 = 0
      FieldWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName28 = 0
      FieldWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName29 = 0
      FieldWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName30 = 0 }

let _ = updateTheRecord4 item

Related information

REPL: 3.7.9.1 Fable: 3.7.9

davidtme avatar Apr 22 '22 10:04 davidtme

Hello @davidtme,

I don't know if you are using inline just for the demo or in your application too.

But using inline, will indeed increase the bundle size each time you use that function because it will be replace in place.

This F# code:

let inline add x y = 
    let result = x + y
    JS.console.log(result)

add 1 1
add 2 2 
add 3 3

generate the following JS:

import { some } from "fable-library/Option.js";

(function () {
    const result = (1 + 1) | 0;
    console.log(some(result));
})();

(function () {
    const result = (2 + 2) | 0;
    console.log(some(result));
})();

(function () {
    const result = (3 + 3) | 0;
    console.log(some(result));
})();

See how the same code is repeated over and over this is because of inline which impact the bundle size.

I don't think that the example you provided is representative.

For me when looking at the generated code 2 things impact the bundle size:

  1. The very long name of the field which cannot compressed by Fable because if are exposing the record to the outside world we can't rename things.
  2. A lot of let item_XXX which are caused by the inline function inside of an inline function.

If we look at the code that Fable is generating for updating a record from a more conventional example:

open Browser
open Fable.Core

type User =
    {
        Firstname : string
        Lastname : string
    }

let updateFirstname (firstname : string) (user : User) =
    { user with
        Firstname = firstname
    }

let updateLastname (lastname : string) (user : User) =
    { user with
        Lastname = lastname
    }

{
    Firstname = "Maxime"
    Lastname = "Mangel"
}
|> updateFirstname "Kaladin"
|> updateFirstname "Kaladin"
|> printfn "%A"
import { Record } from "fable-library/Types.js";
import { record_type, string_type } from "fable-library/Reflection.js";
import { printf, toConsole } from "fable-library/String.js";

export class User extends Record {
    constructor(Firstname, Lastname) {
        super();
        this.Firstname = Firstname;
        this.Lastname = Lastname;
    }
}

export function User$reflection() {
    return record_type("Test.User", [], User, () => [["Firstname", string_type], ["Lastname", string_type]]);
}

export function updateFirstname(firstname, user) {
    return new User(firstname, user.Lastname);
}

export function updateLastname(lastname, user) {
    return new User(user.Firstname, lastname);
}

(function () {
    const arg10 = updateFirstname("Kaladin", updateFirstname("Kaladin", new User("Maxime", "Mangel")));
    toConsole(printf("%A"))(arg10);
})();

What we can see is that when you use the syntax { myRecord with XXX = ... } what fable does is it create a new instance of the type. Meaning that if your record has 200 fields, it will write:

new MyType(old.Field1, old.Field2, old.Field3, old.Field4, old.Field4, ..., , old.Field200)

which is probably one of the source of the increased size. Fable does that types are immutable in F# and so we need a new copy of the time.

@alfonsogarciacaro I wonder, if we could use Object.assign to create a copy of the record and then use mutable code to update the properties that needs to be updated.

I tested this code in the console but I had to remove some Fable addition because I don't have access to the module in the console.

The code place between /* ... */ are code that I removed in my test because I don't have access to Fable modules from the browser console.

class User /*extends Record*/ {
    constructor(Firstname, Lastname) {
        /* super(); */
        this.Firstname = Firstname;
        this.Lastname = Lastname;
    }
}

export function updateFirstname(firstname, user) {
    return Object.assign(Object.create(Object.getPrototypeOf(user)), user, {Firstname: firstname });
}

On such a small record as mine this increase the the bundle size but on record with a few fields it can already reduce the bundle size I think.

My copy function is based on this SO answer https://stackoverflow.com/a/44782052/2911775.

My other idea was to do something like that:

export function updateFirstname(firstname, user) {
    let copy = Object.assign(Object.create(Object.getPrototypeOf(user)), user);
    copy.Firstname = firstname;
    return copy;
}

But I don't know which one is better and I think using the Object.assign with 3 arguments help avoid repeating the:

copy. = ;

again and again so we win a few more chars.

I just don't know if there some implication in making a copy of an object like that.

MangelMaxime avatar Apr 22 '22 15:04 MangelMaxime

Also, I am not sure if Object.assign will create a copy of the nested records or if this keep a reference to the original nested record.

Like in case of:

type Address =
    {
        Street : string
        Zipcode : string
        City : string
    }

type User =
    {
        Name : string
        Address : Address
    }

But perhaps this is not a problem as if the user update one of the field of Address we will create a copy of that specific object.

MangelMaxime avatar Apr 22 '22 15:04 MangelMaxime

@MangelMaxime the inline's and 30 field record is just to show how the problem can grow. I didn't want to post up a 200 line record with 200 updates when something smaller can show the same effect.

davidtme avatar Apr 22 '22 17:04 davidtme

I wonder if fable could create a new method?


export class Record {
    constructor(Field1, Field2) {
        this.Field1 = (Field1 | 0);
        this.Field2 = (Field2 | 0);
    }

    with(fields) {
        new Record(
            fields.hasOwnProperty('Field1') ? fields.Field1 : this.Field1,
            fields.hasOwnProperty('Field2') ? fields.Field1 : this.Field2
        )
    }
}

var a = Record(1,2);
var b = a.with({ Field1: 1 });
var c = b.with({ Field2: 2 });

Because of the performance hit maybe this could be an switched via an attribute on the record. Does fable still have a plugin system, could this be used in some way to override the default behaviour?

davidtme avatar Apr 22 '22 18:04 davidtme

@MangelMaxime the inline's and 30 field record is just to show how the problem can grow. I didn't want to post up a 200 line record with 200 updates when something smaller can show the same effect.

Ok, but using inline doesn't demonstrate the problem you are having in your application because using inline as in the exemple we expect the bundle size to increase that's why I wanted to confirm that you were not didn't it that way.

Fable 3 does have a plugin system but you don't have control over everything. Alfonso will be able to tell you more about it.

MangelMaxime avatar Apr 22 '22 18:04 MangelMaxime

@davidtme Yes, Fable is not very optimized for records with long names. For Fable 3 we were considering to minify record fields (as Elm does I believe) but we discarded because it was complicated, had effects on interop and benefits weren't clear. As @MangelMaxime says, one thing yo should be careful with is inlining functions that may generate lot of code.

If you don't need specifically the properties of a record (comparison, serialization, etc), you can use a class instead like:

type SomeRecord(x, y) =
    member _.FieldWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName1 : int = x
    member _.FieldWithAVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName2 : int = y

In this case Fable will translate the getters as dettached functions and these can indeed be minified so the length of the property names won't affect the bundle size.

@MangelMaxime Using Object.assign could be a good idea. Instead of generating the code in every call-site (which can increase bundle size for small records as you said) we could have a helper similar to:

export function updateRecord(record, key, value) {
    return Object.assign(Object.create(Object.getPrototypeOf(record)), record, {[key]: value });
}

My only concern if whether this could cause deoptimizations by the JS engine as a "typed" object apparently becomes dynamic. Although it's possible JS engines can understand this pattern, so we should measure performance before/after if we introduce this change.

alfonsogarciacaro avatar Apr 26 '22 06:04 alfonsogarciacaro

For Fable 3 we were considering to minify record fields (as Elm does I believe) but we discarded because it was complicated, had effects on interop and benefits weren't clear.

This is one of the reasons why I some time in the past asked whether plugins could be applied on types. I wanted to create a plugin that would minify field names and rewrite callsites for production (since CompiledName has no effect on record fields and you'd have to maintain it manually). Like my recent PR, it's something you can selectively enable at your own risk if you're sure about what you're doing.

kerams avatar Apr 26 '22 06:04 kerams

Another method that may work is:

const copy = new SomeRecord(...Object.values(record));
copy.field = newValue;

This should work as long as the constructor assigns properties in the order that they appear as arguments (the compiler currently does this for record constructors). One caveat is if record has any additional properties added to it (e.g., from JsInterop or dynamic typing), then those will not be present in the copy.

~If we don't want to generate the above code for every call site, this should work as well:~

function updateRecord(record, key, value) {
  const copy = new record.constructor(...Object.values(record));
  copy[key] = value;
  return copy;
}

Nevermind, this won't work if constructor is a property on the record. But the below should:

function updateRecord(record, key, value) {
  const copy = new (Object.getPrototypeOf(record)).constructor(...Object.values(record));
  copy[key] = value;
  return copy;
}

From very minimal testing, this appears to be faster on chromium but slightly slower on firefox compared to Object.assign. However, it appears to be slower than calling the constructor directly with every argument for both browsers.

Unless someone is copying thousands of records, I think either method will work performant-ly. And if they are copying thousands of records, is it safe to assume that the record has only a few fields? In which case, for each record update we could maybe check if the number of fields and/or the combined field name length is long enough to justify doing one of these optimizations.

IanManske avatar Nov 19 '22 02:11 IanManske