Svelte: Max Stack Size Exceeded for huge HTML

Created on 20 Apr 2020  Â·  10Comments  Â·  Source: sveltejs/svelte

Describe the bug
The content of the the svelte file is a large HTML chunk, withouy any script logics or styling being involved. Max stack size exceeded error while compiling.

To Reproduce
https://svelte.dev/repl/a9dfcc17551c4aeb95e8fe748a97061d?version=3.20.1

Expected behavior
Compiling should not break

Information about your Svelte project:
Svelte 3.20.1, Rollup, Windows

Severity
Its probably an uncommon case for most people. I ran into it, when using a generated data privacy HTML template. I would not see this as a priority. Workaround for me: copy the HTML into a JS template literal variable and inject it via @html: https://svelte.dev/repl/1ab32c5a3c2c426e973512cfc8da023c?version=3.20.1

internals good first issue

Most helpful comment

@milahu Thank you for the compiler patch. It works great. With version 3.23.0 of svelte I still had one issue with the patch that resulted in an error because of line 6442 inside comiler.js:

const chunks = node.kind !== 'init'
  ? [c(`${node.kind} `)]
  : [];

Because of the tertiary operator the const declaration does not get replaced with let which leads to an assignment error.

Not sure if it is a good solution or if it leads to other issues with other svelte versions, but the following worked for me. I changed the regex from:

new RegExp("const "+arrayName+" = \\[", 'g'), // global = replace all
    "/* PATCHED const "+arrayName+" */ let "+arrayName+" = ["

to:

new RegExp("const "+arrayName+" = ", 'g'), // global = replace all
    "let "+arrayName+" = "

Hope this helps someone still having troubles.

All 10 comments

the compiling process involves push spreading all nodes into another array at some point and the v8 engine does not support spreading that many items
works fine on firefox

Interesting. I've run into this before in V8 and ended up using an array spread and a reassignment, rather than an argument spread in .push(). https://github.com/Conduitry/do-not-zip/commit/861a929f747f6722dc949d52f146d47c5b7caa87 It's not very helpful that V8 gives a stack size error rather than an argument count error.

I'm not sure how many places we're doing .push(...foo) in the codebase, but this might be a way forward.

Same issue when using Tailwind UI.

In a regular svelte + tailwindcss project if I add in the tailwind.config.js the tailwind UI package plugins: [ require('@tailwindcss/ui'),] then the building breaks with the same issue.

Could we update the issue name as well to include tailwindUI word? @siebeneicher

same here

im building a large single page app / single file app, with many many html nodes

error is

[!] (plugin svelte) RangeError: Maximum call stack size exceeded
src/Main.svelte
RangeError: Maximum call stack size exceeded
    at /tmp/x/node_modules/svelte/node_modules/code-red/dist/code-red.mjs:581:10
    at /tmp/x/node_modules/svelte/node_modules/code-red/dist/code-red.mjs:226:10
    at handle (/tmp/x/node_modules/svelte/node_modules/code-red/dist/code-red.mjs:70:17)
    at /tmp/x/node_modules/svelte/node_modules/code-red/dist/code-red.mjs:248:18
    at Array.map (<anonymous>)
    at handle_body (/tmp/x/node_modules/svelte/node_modules/code-red/dist/code-red.mjs:247:21)
    at Program (/tmp/x/node_modules/svelte/node_modules/code-red/dist/code-red.mjs:318:10)
    at handle (/tmp/x/node_modules/svelte/node_modules/code-red/dist/code-red.mjs:70:17)
    at print (/tmp/x/node_modules/svelte/node_modules/code-red/dist/code-red.mjs:1413:17)
    at Component.generate (/tmp/x/node_modules/svelte/src/compiler/compile/Component.ts:316:9)

with npm install code-red

node_modules/code-red/dist/code-red.mjs line 581

        chunks.push(
            c(') '),
            ...handle(node.body, state)
        );

.... as expected, using push instead of reassign

compiles to node_modules/svelte/compiler.js line 5911

            const chunks = [c('for (')];
            //
            chunks.push(
                c(') '),
                ...handle(node.body, state)
            );

todo: push ----> reassign

like

            let chunks = [c('for (')];
            //
            chunks = [
                ...chunks,
                c(') '),
                ...handle(node.body, state)
            ];

my svelte compiler is fixed

todo: push ----> reassign

here is a quick-and-dirty patch script

/*
  src/patch-svelte-compiler.js

  replace push with reassign

  cp src/patch-svelte-compiler.js node_modules/svelte/
  cd node_modules/svelte
  node patch-svelte-compiler.js

*/

const base_file = "compiler.js";

const input_file = base_file + ".orig";
const output_file = base_file + ".new";

// replaceMethod
//   origin: a.push(...a1, ...a2, e, ...a3); // error: Max Stack Size Exceeded
//   spread: a = [...a1, ...a2, e, ...a3];
//   concat: a = a1.concat(a2, [e], a3);
//   performance is equal on nodejs
const replaceMethod = "spread";
//const replaceMethod = "concat";

const acorn_parse = require("acorn").parse;
const estree_walk = require("estree-walker").walk;
const node_tosource = require("tosource");
const magicString = require("magic-string");
const fs = require("fs");

if (fs.existsSync(input_file) || fs.existsSync(output_file)) {
  console.log('error: input or output file exists. run this script only once');
  process.exit(1);
}

console.log(`move file: ${base_file} --> ${input_file}`)
fs.renameSync(base_file, input_file);

// input
const content = fs.readFileSync(input_file, 'utf8');

// output
let code = new magicString(content);

const ast = acorn_parse(
  content, {
  // ecmaVersion: 10, // default in year 2019
  sourceType: 'module',
});

const funcName = "push";

let arrayNameList = [];

estree_walk( ast, {
  enter: function ( node, parent, prop, index ) {

    // node must be array.push()
    if (
      node.type !== 'CallExpression' ||
      node.callee === undefined ||
      node.callee.property === undefined ||
      node.callee.property.name !== funcName
    ) { return; }

    // argument list must include spread operators
    if (node.arguments.find(
      a => (a.type == 'SpreadElement')) === undefined)
    { return; }

    const nodeSrc = content.substring(node.start, node.end);

    const pushObj = node.callee.object;
    const arrayName = content.substring(pushObj.start, pushObj.end);

    const pushProp = node.callee.property;

    arrayNameList.push(arrayName);

    // patch .push(

    if (replaceMethod == "spread") {
      // push --> assign array

      // find "(" bracket after .push
      const pushPropLen = content.substring(pushProp.start, node.end).indexOf("(");

      code.overwrite(
        (pushProp.start - 1),
        (pushProp.start + pushPropLen + 1),
        " /* PATCHED */ = [..."+arrayName+", "
      );

      // patch closing bracket
      const closeIdx = node.start + nodeSrc.lastIndexOf(")");
      code.overwrite(closeIdx, (closeIdx + 1), "]");
    }

    if (replaceMethod == "concat") {
      // push --> assign concat
      // ".push" --> " = array.concat"
      code.overwrite(
        (pushProp.start - 1),
        pushProp.end,
        " /* PATCHED */ = "+arrayName+".concat");

      // patch arguments of .concat()
      node.arguments.forEach(a => {
        if (a.type == 'SpreadElement') {
          // unspread: ...array --> array
          const spreadArgSrc = content.substring(a.argument.start, a.argument.end);
          //console.log('spread argument: '+spreadArgSrc);
          code.overwrite(a.start, a.end, spreadArgSrc);

        } else {
          // enlist: element --> [element]
          const argSrc = content.substring(a.start, a.end);
          //console.log('non spread argument: '+argSrc);
          code.overwrite(a.start, a.end, "["+argSrc+"]");
        }
      });
    }

}});

code = code.toString();

function filterUnique(value, index, array) { 
  return array.indexOf(value) === index;
}

// replace const with let
arrayNameList.filter(filterUnique).forEach(arrayName => {
  console.log(`arrayName = ${arrayName}`)

  code = code.replace(
    new RegExp("const "+arrayName+" = ", 'g'), // global = replace all
    "/* PATCHED const "+arrayName+" */ let "+arrayName+" = "
  );
})

fs.writeFileSync(output_file, code);

console.log(`move file: ${output_file} --> ${base_file}`)
fs.renameSync(output_file, base_file);

edit: included patch by rhythm-section
edit: move input/output files in javascript
edit: added replaceMethod = "concat", but performance is equal

@milahu Thank you for the compiler patch. It works great. With version 3.23.0 of svelte I still had one issue with the patch that resulted in an error because of line 6442 inside comiler.js:

const chunks = node.kind !== 'init'
  ? [c(`${node.kind} `)]
  : [];

Because of the tertiary operator the const declaration does not get replaced with let which leads to an assignment error.

Not sure if it is a good solution or if it leads to other issues with other svelte versions, but the following worked for me. I changed the regex from:

new RegExp("const "+arrayName+" = \\[", 'g'), // global = replace all
    "/* PATCHED const "+arrayName+" */ let "+arrayName+" = ["

to:

new RegExp("const "+arrayName+" = ", 'g'), // global = replace all
    "let "+arrayName+" = "

Hope this helps someone still having troubles.

Is there any CI-able solution?

you mean patching the typescript source files?

here is a start:
https://github.com/milahu/random/blob/master/svelte/patch-svelte-compiler-sources.js

todo:

  • find source files of svelte/compiler.js via sourcemaps
  • if needed, add javascript transformer in second patch pass
  • send PRs to affected libraries

cosmetic:

  • avoid second patch pass, use node.parent to find declaration in local scope

the problem with concat is: concat is slower than push
= overhead of immutable data

this should be fastest:

Array.prototype._concat_inplace = function(other) { // aka _push_array
  for (let i = 0; i < other.length; i++) {
    this.push(other[i]);
  }
  return this; // chainable
};

array1._concat_inplace(array2)._concat_inplace(array3);

its even faster than array1.push(...array2)
and it is not limited by max call stack size (node ~100K, firefox ~500K)

This issue is affecting working with .svg aswell. I have a bunch of huge SVGs that I want to animate with Svelte (in Sapper). Unfortunately I run into

✗ client
Maximum call stack size exceeded

My workaround is to downsize the svg, by reducing the complexity of paths used in it.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

juniorsd picture juniorsd  Â·  3Comments

noypiscripter picture noypiscripter  Â·  3Comments

plumpNation picture plumpNation  Â·  3Comments

st-schneider picture st-schneider  Â·  3Comments

AntoninBeaufort picture AntoninBeaufort  Â·  3Comments