Why building a 2D array first matters
The instinct when expanding rows is to call insertRowAfter inside the loop — it feels direct. The problem is that every insertRowAfter is a separate API call to Sheets' backend, and Apps Script has a hard quota of about 6 minutes of execution per run. With 500 source rows averaging 4 values each, you're looking at 2,000 insertRowAfter calls. That's when the script starts timing out. The first time I hit this, I had a product catalog import that ran fine in testing (50 rows) and died silently in production (800 rows) — no error surfaced, just a truncated sheet.
The fix is to treat the Sheets API as a write-once surface. Read everything in one getValues call, build the full output array in plain JavaScript memory, then write it back in one setValues call. JavaScript array operations have no quota cost; only the two Sheets calls count against your limits.
The script above does exactly this. getRange(2, 1, src.getLastRow() - 1, 1).getValues() reads column A from row 2 to the end in a single round-trip. The nested loop builds out, a flat 2D array where every element is a one-item array (the shape setValues expects). Then one dest.getRange(...).setValues(out) writes the whole thing.