Diagnosing Sluggish Scrolling, Blank Rows, and Crashes in Ti.UI.TableView with Large Data Sets in Titanium SDK
Learn how to diagnose and fix sluggish scrolling, blank rows, and crashes in Ti.UI.TableView when handling large data sets in Titanium SDK apps.
07 Aug 2026, 07:26 UTC

Recognizable condition
When a Titanium SDK app displays a large data set in a Ti.UI.TableView, users may notice one or more of the following:
- Scrolling feels jerky or drops below 60 fps.
- Rows appear blank intermittently while scrolling.
- The app crashes or throws an exception after navigating away from the table view screen.
Cause / diagnostic table
| Symptom | Possible cause | Quick check |
|---|---|---|
| Sluggish scrolling | Too many rows rendered at once; row recycling not working | Verify that only the visible rows are instantiated and that Ti.UI.TableView.SELECTION_NONE is set if selection is not needed. |
| Blank rows | Data source length mismatch or missing row template | Log the length of the data array right before calling setData. |
| Crash on exit | Lingering event listeners or memory leak on the table view or its children | Check for any addEventListener calls that are not paired with a removeEventListener in the window’s close or blur handler. |
Ordered checks
Enable verbose logging in the data‑source setup to confirm the expected row count.
// Run in the controller where the TableView is populated (e.g., index.js) Ti.API.info('Data source length: ' + data.length); // data is the array passed to tableView.setData(data);Where to run: in the Titanium Alloy controller or classic
app.jsfile. No special permissions are required beyond the normal build environment.Expected check: the logged length matches the number of items you intend to display. A significantly larger number indicates duplicated data; a smaller number suggests missing rows.
Risk: none – logging only adds console output.
Profile UI thread frame time while scrolling to see if the main thread exceeds the 16 ms budget (≈60 fps).
Where to run: Android Studio Profiler (for Android builds) or Instruments → Core Animation (for iOS builds). You need the device or emulator connected and the app running in debug mode.
Expected check: average frame time ≤ 16 ms during steady scrolling. Spikes > 16 ms point to heavy work on the UI thread.
Risk: profiling may slightly affect performance; use a release‑like build for realistic numbers.
Inspect row reuse by assigning a unique identifier to each row and logging when a row is created versus reused.
// Example using className for recycling (Alloy) var rows = []; data.forEach(function(item, index) { var row = Ti.UI.createTableViewRow({ className: 'itemRow', // important for recycling height: Ti.UI.SIZE, itemId: index // optional custom property }); var label = Ti.UI.createLabel({ text: item.title }); row.add(label); rows.push(row); }); tableView.setData(rows); // Log creation vs reuse tableView.addEventListener('postlayout', function e() { Ti.API.info('Row count after layout: ' + tableView.data[0].rowCount); });Where to run: in the same controller where rows are built. No extra permissions needed.
Expected check: after the initial load, scrolling should not produce new “Row created” logs; only reuse logs should appear. If you see continuous creation logs, recycling is broken.
Risk: adding a
postlayoutlistener adds minimal overhead; remove it after debugging.Verify that all custom event listeners attached to the table view or its rows are removed when the window closes.
// In the window’s close handler win.addEventListener('close', function() { tableView.removeEventListener('click', onRowClick); tableView.removeEventListener('scroll', onScroll); // If you used Ti.App listeners, remove them as well Ti.App.removeEventListener('customEvent', onCustom); });Where to run: in the window controller that hosts the TableView.
Expected check: after navigating away, no listeners remain attached to the table view (you can verify by temporarily adding a debug listener that logs when fired and confirming it does not fire after close).
Risk: forgetting to remove a listener can cause memory leaks; the fix is safe as long as you match the exact callback reference.
Test on both simulator/emulator and a physical device covering the OS versions you support (e.g., Android 5.0+, iOS 12+).
Where to run: Titanium CLI
ti build -p android -b deviceandti build -p ios -b deviceor the respective IDEs.Expected check: the symptoms should be absent or markedly reduced on real hardware.
Risk: device testing may reveal issues hidden by simulators; ensure you have a representative low‑end device for worst‑case validation.
Fixes tied to findings
If the data‑source log shows a row count far exceeding the visible rows (e.g., 5000 rows while only 10 are visible), implement batch loading or update the table incrementally:
// Pseudocode for batch loading var batchSize = 20; var offset = 0; function loadNextBatch() { var batch = data.slice(offset, offset + batchSize); offset += batchSize; var rows = batch.map(function(item) { var r = Ti.UI.createTableViewRow({ className: 'itemRow' }); r.add(Ti.UI.createLabel({ text: item.title })); return r; }); tableView.appendRow(rows); // or insertRows if you need precise indices if (offset < data.length) { setTimeout(loadNextBatch, 10); } } loadNextBatch();Where to run: in the controller after the initial data fetch.
Expected check: UI thread frame time drops below 16 ms and memory growth stabilizes.
Risk: batch loading introduces latency for the initial view; tune
batchSizeto balance startup speed and smoothness.If profiling shows UI thread work > 16 ms, simplify row layouts:
- Avoid nested
Viewhierarchies; use a singleViewwith abackgroundImagewhere possible. - Replace image‑heavy rows with compressed assets or use
Ti.UI.ImageViewwithdefaultImage. - Set
selectionStyle: Ti.UI.iPhone.TableViewCellSelectionStyle.NONEon iOS if selection is unnecessary.
Where to run: in the row‑creation loop.
Expected check: frame time returns to ≤ 16 ms during scrolling.
Risk: over‑simplification may remove needed UI elements; verify visual fidelity after each change.
- Avoid nested
If row‑reuse logs indicate missing reuse, ensure each row has a stable
className(orrowIdin classic Titanium) and avoid recreating rows inside scroll listeners.Where to run: same as step 3 above.
Expected check: reuse count matches the number of visible rows after the initial load.
Risk: changing
classNameafter rows are created breaks recycling; keep it constant for the lifetime of the table.If listeners remain attached after window close, explicitly remove them as shown in step 4, or use the
removeAllListenershelper if available.Where to run: window’s
closeorblurhandler.Expected check: no listener callbacks fire after navigation away.
Risk: removing the wrong callback can break functionality; keep a reference to the exact function used in
addEventListener.If crashes persist on a specific OS version after applying the above, update to the latest Titanium SDK patch (e.g., 9.2.2 → 9.3.0) and perform a clean rebuild:
ti sdk install latest ti cleanWhere to run: in the project directory via the Titanium CLI.
Expected check: crash logs no longer show native
TiUITableView(iOS) orTiTableView(Android) stack traces.Risk: a new SDK may introduce other regressions; test core functionality after the upgrade.
Escalation criteria
Escalate to the Titanium community or file a support ticket when any of the following is true:
- The issue reproduces in a clean, minimal project that contains only the TableView code and the data‑source logic.
- Profiling shows garbage‑collection pauses > 120 ms despite all UI‑thread optimizations.
- Crash logs contain a native stack trace pointing to
TiUITableView(iOS) orTiTableView(Android). - The problem remains after applying all fixes listed above and rebuilding with the latest SDK patch.
When escalating, include:
- SDK version and build hash.
- Device/emulator OS version and model.
- Relevant log snippets (Ti.API.info, profiler output, native crash trace).
- A minimal reproducible example (e.g., a GitHub gist with the controller and
app.xml).
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.