Client scripts work inconsistently

Hi,
I have noticed that my custom scripts have started failing to execute occasionally when loading forms. Below are the warnings that I get in the console when the issue occurs:

[Violation] 'load' handler took 200ms
[Violation] Forced reflow while executing JavaScript took 47ms

I’m at a complete loss for ideas to resolve this issue. I would greatly appreciate any help that can be provided.

It sounds like your scripts are drawing too many resources to process effectively. Can you share the script? We could help troubleshoot more effectively if you did.

Thanks for your time. Please see the below:

frappe.require(['/assets/eskill_custom/js/common.js', '/assets/eskill_custom/js/selling.js'], () => {
    frappe.ui.form.on('Sales Invoice', {
        refresh(frm) {
            stock_item_filter(frm);
            tax_template_filter(frm);
            setTimeout(() => {
                frm.remove_custom_button("Work Order", 'Create');
                frm.remove_custom_button("Project", 'Create'); 
                frm.remove_custom_button("Subscription", 'Create');
                frm.remove_custom_button("Return / Credit Note", 'Create');
            }, 500);
            naming_series_set(frm);
        },
        
        before_save(frm) {
            set_tax_template(frm);
            assign_sales_person(frm);
            if (frm.doc.stock_item) {
                frm.doc.stock_item = undefined;
            }
            get_bid_rate(frm, frm.doc.posting_date);
            limit_rate(frm);
        },

        before_submit(frm) {
            assign_sales_person(frm);
        },
        
        after_save(frm) {
            if (frm.doc.issue){
                issue_billing_update(frm, "invoice");
            }
        },
        
        on_submit(frm) {
            link_credit_to_invoice(frm);
            if (frm.doc.issue){
                issue_billing_update(frm, "invoice");
            }
        },
        
        on_update(frm) {
            if (frm.doc.issue){
                issue_billing_update(frm, "invoice");
            }
        },
        
        after_cancel(frm) {
            if (frm.doc.issue){
                issue_billing_update(frm, "invoice");
            }
        },

        conversion_rate: function(frm) {
            limit_rate(frm);
            convert_selected_to_base(frm);
        },

        currency: function(frm) {
            get_bid_rate(frm, frm.doc.posting_date);
            if (frm.doc.customer) {
                set_tax_template(frm);
            }
        },

        customer: function(frm) {
            set_tax_template(frm);
        },

        is_return: function(frm) {
            naming_series_set(frm);
        },

        posting_date: function(frm) {
            if (frm.doc.posting_date) {
                get_bid_rate(frm, frm.doc.posting_date);
            }
        },

        search: function(frm) {
            if (frm.doc.stock_item) {
                stock_lookup(frm);
            } else {
                frappe.throw("You must select a stocked item before performing a stock lookup.");
            }
        },

        usd_to_currency: function(frm) {
            convert_base_to_selected(frm);
        }
    });

    function link_credit_to_invoice(frm) {
        if (frm.doc.is_return) {
            frappe.call({
                method: "eskill_custom.api.set_invoice_as_credited",
                args: {
                    credit: frm.doc.name
                },
                callback: function (message) {
                    if (message) {
                        console.log(message);
                    }
                }
            });
        }
    }

    function naming_series_set(frm) {
        if (frm.doc.is_return) {
            frm.set_value("naming_series", "CN.########");
        } else {
            frm.set_value("naming_series", "SI.########");
        }
    }
});

The first thing that jumps out at me is the setTimeout in your refresh function. I don’t know much offhand about how load functions work, but it seems possible that the timeout is interrupting rendering as per your error message.

I’ve never needed to use a timeout to use default action buttons. Are you sure it’s necessary here? If you remove it, do you still get the error?

I had put in the timeout because without it the buttons were being rendered after the functions to remove the built in buttons are executed. Perhaps I should look for a different trigger to execute those functions on. I just went with the timeout as that was the suggested method that I found on the forums.

One thing to add is that the problem has only started popping up recently. The most recent change is the frappe.require() method at the top, as I have moved all of the common functions across my scripts to library files. They do not contain any code that was not already in the scripts.

Ah, yes, I didn’t notice that before, but I’d strongly suspect that’s the issue. I don’t know much about frappe.require, but I assume it’s a wrapper around the nodejs method. If that’s true, it would explain the error you’re getting. If you want to include a library, probably better to do it the old fashioned way with hooks.

Alright, I will try using hooks instead of frappe.require() and I will get back to you. It might be that using frappe.require() is too much for the handler when used for the entire form as the examples used in the documentation indicate it being used inside of events and functions.

Hi @peterg. I’m sorry that it has taken me so long to get back to you- I haven’t had the chance to try using the hooks like we discussed until now. I am struggling a bit with the hooks and I was hoping that you might know the solution:

When I use the hooks to extend the the form script it works fine but if I try to hook in a script that just has functions and call the functions from a client script, I get a reference error stating that the function is undefined.

I found my solution by tinkering- I tried doing normal imports but that fails because the scripts aren’t hooked in as modules. The solution is frappe.require() but it has to be placed at the top, then continue with your script below it. An example using the code sample that I provided above:

frappe.require([
    '/assets/eskill_custom/js/common.js',
    '/assets/eskill_custom/js/selling.js'
]);

frappe.ui.form.on('Sales Invoice', {
    refresh(frm) {
        stock_item_filter(frm);
        tax_template_filter(frm);
        setTimeout(() => {
            frm.remove_custom_button("Work Order", 'Create');
            frm.remove_custom_button("Project", 'Create'); 
            frm.remove_custom_button("Subscription", 'Create');
            frm.remove_custom_button("Return / Credit Note", 'Create');
        }, 500);
        naming_series_set(frm);
    },
    
    before_save(frm) {
        set_tax_template(frm);
        assign_sales_person(frm);
        if (frm.doc.stock_item) {
            frm.doc.stock_item = undefined;
        }
        get_bid_rate(frm, frm.doc.posting_date);
        limit_rate(frm);
    },

    before_submit(frm) {
        assign_sales_person(frm);
    },
    
    after_save(frm) {
        if (frm.doc.issue){
            issue_billing_update(frm, "invoice");
        }
    },
    
    on_submit(frm) {
        link_credit_to_invoice(frm);
        if (frm.doc.issue){
            issue_billing_update(frm, "invoice");
        }
    },
    
    on_update(frm) {
        if (frm.doc.issue){
            issue_billing_update(frm, "invoice");
        }
    },
    
    after_cancel(frm) {
        if (frm.doc.issue){
            issue_billing_update(frm, "invoice");
        }
    },

    conversion_rate: function(frm) {
        limit_rate(frm);
        convert_selected_to_base(frm);
    },

    currency: function(frm) {
        get_bid_rate(frm, frm.doc.posting_date);
        if (frm.doc.customer) {
            set_tax_template(frm);
        }
    },

    customer: function(frm) {
        set_tax_template(frm);
    },

    is_return: function(frm) {
        naming_series_set(frm);
    },

    posting_date: function(frm) {
        if (frm.doc.posting_date) {
            get_bid_rate(frm, frm.doc.posting_date);
        }
    },

    search: function(frm) {
        if (frm.doc.stock_item) {
            stock_lookup(frm);
        } else {
            frappe.throw("You must select a stocked item before performing a stock lookup.");
        }
    },

    usd_to_currency: function(frm) {
        convert_base_to_selected(frm);
    }
});

function link_credit_to_invoice(frm) {
    if (frm.doc.is_return) {
        frappe.call({
            method: "eskill_custom.api.set_invoice_as_credited",
            args: {
                credit: frm.doc.name
            },
            callback: function (message) {
                if (message) {
                    console.log(message);
                }
            }
        });
    }
}

function naming_series_set(frm) {
    if (frm.doc.is_return) {
        frm.set_value("naming_series", "CN.########");
    } else {
        frm.set_value("naming_series", "SI.########");
    }
}
2 Likes