Bug in Sketch 2025.3.4: Exporting Slice inside Symbol via sketch.expor

In Sketch version 2025.3.4, I encountered an export issue: When a Slice is directly on an Artboard, export works normally. However, when the Slice is nested inside a Symbol, using sketch.export() to export in PNG or WebP format produces blank (fully transparent or empty content) image files — even though the file dimensions are correct. Exporting to SVG format works perfectly and includes the expected content.

Could this be related to the fix in this update:
“Fixes a performance issue in documents containing hidden Symbol instances that no longer references a valid Symbol.”?

Steps to Reproduce:

  1. Create a Symbol (e.g., containing shapes or text).

  2. Inside the Symbol source page, use the Slice tool to create a Slice over the content you want to export.

  3. Go back to an Artboard and insert an instance of that Symbol.

  4. Use a script or plugin to call sketch.export() on the Slice inside the Symbol instance, specifying PNG or WebP format.

  5. Check the exported files.

Actual Result:

  • PNG/WebP: File size is correct, but content is blank/empty.

  • SVG: Content exports correctly.

Expected Result:
PNG/WebP should export the visible content properly, just like SVG, regardless of whether the Slice is nested in a Symbol.

Environment:

  • Sketch Version: 2025.3.4

Thank you for the great work on Sketch — looking forward to a fix!

slice-bug.sketch (16.3 KB)

Follow-up: More precise bug location identified
I’ve now pinpointed the exact trigger for this bug:
The document includes an external/detached Symbol that is no longer present in the current document (library path: “kk_icon/tips/tips_menu”). A Slice named “kk_tips_menu” was added at the same level (sibling) as this Symbol instance.
In Sketch 2025.3.4, when using sketch.export() on this Slice:

PNG and WebP formats: The exported image is completely transparent/blank (no visible content), though the file dimensions are correct.
SVG format: Exports correctly with full content.

This seems very likely related to the changelog fix in 2025.3.4:
“Fixes a performance issue in documents containing hidden Symbol instances that no longer references a valid Symbol.”
It matches the scenario of a “Symbol instance that no longer references a valid Symbol,” which may be causing raster export (PNG/WebP) to fail rendering, while vector (SVG) remains unaffected.
Attachment: I’ve uploaded a minimal reproducible .sketch demo file (includes the detached Symbol reference + sibling Slice). Engineers can simply select the Slice and run sketch.export() to reproduce the issue.
Thanks for looking into this — hoping this helps confirm if it’s a side effect of that performance fix, and looking forward to a resolution in a future update!

Hello there and thanks so much for such a detailed bug report!

Use a script or plugin to call sketch.export() on the Slice inside the Symbol instance

May I ask you to share the exact code you’re using to grab this slice from the symbol instance? I’m asking because technically the instance doesn’t contain this slice – it belongs to the master, which may or may not affect asset generation.

Here is a simplified implementation of my code, where I used “symbolInstance.duplicate();” to structure the SymbolInstance into a Group before using it.

main.js

const sketch = require('sketch/dom');
const fs = require('@skpm/fs');

function rndShortHexValue() {
    return `${Math.random().toString(36).substring(2)}`;
}
function getDocumentArtboards(doc) {
    let list = [];
    if (!doc) return list;
    let pages = doc.pages;
    if (!pages || pages.length < 1) return list;
    pages.forEach(page => {
        let layers = page.layers;
        if (!layers || layers.length < 1) return;
        layers.forEach(layer => {
            if (!layer || layer.type !== "Artboard") return;
            list.push(layer);
        });
    });
    return list;
}
function getSelectedDocument(context) {
    let doc;
    if (context) {
        doc = context.actionContext?.document || context.document;
    }
    return doc ? sketch.fromNative(doc) : sketch.getSelectedDocument();
}

function isFolderExist(folderpath){
    try {
        let stat = fs.statSync(folderpath);
        return stat.isDirectory();
    } catch (error) {
        return false;
    }
}
function createSubFolder(folderpath){
    try {
        if(isFolderExist(folderpath))return true;
        fs.mkdirSync(folderpath);
        return true;
    } catch (error) {
        console.log(error)
        return false;
    }
}

const exportOptions = {
    compact: false,
    'include-namespaces': false,
    'group-contents-only': true,
    overwriting: true,
    progressive: false,
    'save-for-web': true,
    'use-id-for-name': true,
    trimmed: false, // 等价于 shouldTrim=false
    compression: 1.0,
};
/**
 * main
 */
export function exportMain(context) {
    const document = getSelectedDocument();
    if (!document) {
        console.log("Please open the document first.");
        return;
    }
    const pluginFolderPath = context.scriptURL.URLByDeletingLastPathComponent().path();
    const paths = [`export_output`, rndShortHexValue()];
    let outputFold = pluginFolderPath + "/";
    paths.forEach(path => {
        outputFold += path + "/";
        if (!createSubFolder(outputFold)) {
            console.log(`Failed to create temporary export directory ${outputFold}`);
            return;
        }
    });
    console.log(`layer will export in :${outputFold}`)
    const allArtboards = getDocumentArtboards(document);
    const exportLayers = [];
    for (let index = 0; index < allArtboards.length; index++) {
        const artboard = allArtboards[index];
        exportLayers.push(...getAllExportLayers(artboard, artboard, outputFold));
    }
    console.log("exportLayers", exportLayers);
}
function getAllExportLayers(artboard, layer, outputFold) {
    const list = [];
    let markedForExport = false;
    const { type, layers, name, id } = layer;
    if (type !== "Artboard") {
        let exportFormats = layer.exportFormats;
        markedForExport = exportFormats && exportFormats.length > 0;
    }
    if (markedForExport) {
        const success = sketch.export(layer, {
            ...exportOptions,
            output: outputFold,
            scales: [1, 2, 3],
            formats: "png"
        }) && sketch.export(layer, {
            ...exportOptions,
            output: outputFold,
            formats: "svg"
        });
        console.log(`export layer:[${layer.type}] ${layer.name},success=${success}`);
        list.push({ id, name, type });
    }
    else if (type === "SymbolInstance") {
        list.push(...exportSymbolInstance(artboard, layer, outputFold))
        return list;
    }
    if (layers && layers.length > 0) {
        for (let index = 0; index < layers.length; index++) {
            list.push(...getAllExportLayers(artboard, layers[index], outputFold));
        }
    }
    return list;
}
function exportSymbolInstance(artboard, symbolInstance, outputFold) {
    let tempGroup;
    try {
        tempGroup = symbolInstance.duplicate();
        tempGroup.parent = artboard;
        let symbolFrame = symbolInstance.frame.toJSON();
        tempGroup.frame = symbolFrame;
        let tempGroup2 = tempGroup.detach({ recursively: false });
        if (tempGroup2) {
            tempGroup = tempGroup2;
        }
        return getAllExportLayers(artboard, tempGroup, outputFold);
    } finally {
        if (tempGroup) {
            tempGroup.remove();
            tempGroup = null;
        }
    }
}
export default function () {
    //IS_DEV && console.log(`${timeFormat()}:onRun.default`)
};

manifest.json

{
  "$schema": "https://raw.githubusercontent.com/sketch-hq/SketchAPI/develop/docs/sketch-plugin-manifest-schema.json",
  "commands": [
    {
      "name": "export layers",
      "identifier": "exportMain",
      "script": "./main.js",
      "handlers": {
        "run":"exportMain"
      }
    }
  ],
  "menu": { 
    "title": "slice_bug_report",
    "items": [
      "exportMain"
    ]
  }
}

1 Like

Thanks so much! I took your code snippet a little bit further, trying to reduce the test case to the bare minimum:

const sketch = require('sketch/dom')
const document = sketch.getSelectedDocument()

const artboard = document.selectedPage.canvasLevelFrames[0]
const symbolInstance = sketch.find('SymbolInstance', artboard)[0]

let tempGroup = symbolInstance.duplicate()
tempGroup = tempGroup.detach({ recursively: false })

const slice = sketch.find('Slice', tempGroup)[0]
sketch.export(slice, {
  output: "/Users/rodionovd/Desktop/slice_export_issue",
  formats: "png"
})

tempGroup.remove()

And indeed, this got me a blank image as a result:

Now, after experimenting a bit I noticed that if I flip the recursive flag to true in

tempGroup = tempGroup.detach({
-    recursively: false
+    recursively: true
})

the generated PNG is no longer empty:

Which suggest that it’s this nested symbol instance that’s causing problems and not the slice itself:

Workaround

Now, my theory is that Sketch doesn’t get enough time – between detaching the top-level symbol and exporting the slice – to update its internal symbol cache, which results in the nested symbol rendering as a blank image.

There’re two ways you can approach this problem:

  1. I’d recommend using detach({ recursive: true }) so that all nested symbols are also detached and won’t cause issues during export.

  2. Alternatively, you may force Sketch to update its symbol cache by doing something like this:

    let tempGroup = symbolInstance.duplicate()
    tempGroup = tempGroup.detach({ recursively: false })
    
     // 👇 flush cached rendition for the nested symbol by "updating" its visibility
    const nestedSymbol = sketch.find('SymbolInstance', tempGroup)[0]
    nestedSymbol.hidden = nestedSymbol.hidden
    
    const slice = sketch.find('Slice', tempGroup)[0]
    slice.export(...) // etc
    

Thank you so much for your reply and suggestions! Solution 2 worked perfectly and solved my issue.
Regarding Solution 1, I think detach({ recursively: true }) can be quite expensive performance-wise, especially when dealing with deeply nested symbols in complex files (recursively detaching everything tends to be time-consuming). So I prefer Solution 2 (detaching only the current level + handling deeper levels manually or avoiding full recursion), which is more controllable and efficient.
Thanks again — the problem is completely fixed now! :grinning_face:

1 Like

Ah this is a valid concern of course!

By the way, I’ll see to it that Solution #2 is built into SketchAPI directly so you won’t need it in the future!

I also noticed the theory you described: “Sketch doesn’t get enough time – between detaching the top-level symbol and exporting the slice” to properly sync its internal state or flush any caching/rendering.
Inspired by that, I experimented with Scheme 3: adding a short intentional delay right after a successful detach to give Sketch a moment to process:
let tempGroup2 = tempGroup.detach({ recursively: false });
// console.log(“11111111111111111, tempGroup2 is null”, tempGroup2 == null);
if (tempGroup2) {
tempGroup = tempGroup2;
await threadSleep(33); // ~33ms yield to allow internal processing
}
Initial tests show the exported slices are now coming out correctly—no stale cache issues or inconsistencies.
I’ll run more extensive tests across various file sizes, nesting levels, and batch exports. If it proves reliable, this feels like the most economical fix:

Avoids the heavy performance hit of recursively: true
No need for detach + re-insert (preserves overrides and keeps logic simple)
A tiny delay (33ms is negligible) is all it takes to let Sketch catch up internally

Since you mentioned potential optimizations in future versions for this kind of timing issue, I’ll make the delay conditional — only applying it on 2025.3.4 — so it can be safely removed once a fix lands in a later release.
Thanks again for the insightful theory — it really helped narrow this down quickly! I’ll share more test results as I go.

1 Like

I tried Scheme 3 with await threadSleep(33) after detach, but further tests show it’s not reliable enough:

Exporting while on the Symbols page results in a transparent slice (or stale content).
Switching back to the Artboard page makes it work immediately.

So timer-based delays don’t consistently force the refresh, especially in inactive views.
For now, Scheme 2 seems the more dependable option, even if a bit more involved.
Looking forward to the new Sketch version!

This topic was automatically closed 3 days after the last reply. New replies are no longer allowed.