Skip to content
Back to guides
Model checks

XGBoost vs TF.js Bricks: Which to Use When

Shark Snip Editorial 10 min read

Read the price, role, and market first

A head-to-head guide for choosing between XGBoost and TF.js bricks for betting models — accuracy, latency, memory, and browser portability.
18 sections
XGBoost vs TF.js Bricks: Which to Use When cover art

Both XGBoost and TensorFlow.js can power a perfectly good in-browser betting model. They are not equivalent. They behave differently under realistic browser constraints, they handle different feature shapes well, and they fail in different ways. This post is a head-to-head playbook for choosing between them when you build a new brick on Workshop or Tinker.

The one-line summary

If your features are clean and tabular and your dataset is under 10,000 rows, default to XGBoost. If your features include sequences (last-N games, play-by-play), if you need sub-20ms inference for live betting, or if you need the model to fit on a phone, default to TF.js. If you're stacking them, use the recipe in stacking models in Workshop.

Accuracy: where each wins

For the kind of dataset a retail sports bettor builds — 1,000 to 5,000 games, 15 to 30 engineered features — XGBoost almost always edges a comparable TF.js net by 1 to 3 percent in log loss. Concrete numbers from our NFL spread backtest:

  • XGBoost (500 trees, depth 6, lr 0.05): Brier 0.2331, log loss 0.6648 on 2024 holdout.
  • TF.js (64-32-16 dense, dropout 0.2, 200 epochs): Brier 0.2356, log loss 0.6688.

That 0.4% log-loss gap is small but consistent across NFL, NBA, and MLB datasets. It comes from XGBoost's ability to find interactions and threshold effects in tabular features automatically. The neural net has to be coached into the same behavior, and dense layers are not the right tool when you already know the splits matter.

The flip happens on richer inputs. If you feed each model the last six game-level box scores as a sequence rather than as pre-aggregated rolling averages, the TF.js net (especially with a small GRU front end) starts to match and then beat XGBoost. The trees can't learn temporal structure without you spoon-feeding it; the net learns it from data.

Sample size matters more than people think

Under 500 training games, XGBoost overfits badly unless heavily regularized. TF.js with dropout 0.3 and early stopping is more forgiving in small-sample regimes. Above 3,000 games, XGBoost's accuracy advantage is meaningful and stable. Between those two regimes, the choice is a coin flip that depends on feature quality.

Latency: where TF.js dominates

Inference latency on a typical laptop CPU, batch of 16 NFL games:

  • XGBoost (500-tree, JS port): roughly 80ms.
  • XGBoost (500-tree, WASM port): roughly 28ms.
  • TF.js dense (64-32-16): roughly 12ms.
  • TF.js dense with WebGL backend: roughly 4ms.

For weekly card prep these numbers don't matter. For an in-play widget on the picks page that needs to update on every line move, 80ms vs 4ms is the difference between snappy and laggy. The other dimension is mobile: on a mid-range phone CPU, XGBoost JS inference can balloon to 300ms+ per batch. TF.js with the WebGL backend stays under 30ms on the same phone.

Cold start matters more than steady state

Loading a 90MB XGBoost JSON into memory takes about 700ms on a fast laptop and well over 2 seconds on a phone over LTE. A 4MB TF.js model loads in roughly 90ms with a warm cache. For pages where you want time-to-first-prediction under a second, TF.js is the only option without backend support.

Memory: the silent budget

The big trap with XGBoost in the browser is RAM. A trained XGBoost model with 500 trees and depth 6 typically lands at 70–120MB of in-memory JSON. During prediction it spikes to 1.5x that due to internal copies. On a laptop with 16GB total this is invisible. On a phone with 4GB shared across the browser, OS, and other tabs, it's a tab-killer.

TF.js models stay tiny by comparison. A dense net with 4,000 parameters serialized is about 16KB, plus a few hundred KB of TF.js runtime. Even a deeper net stays under 10MB resident. If your brick has to ship to mobile users, TF.js is the right default unless you have a hard accuracy constraint that only XGBoost meets.

Quantization helps both, but not equally

Quantizing XGBoost split values to 3-bit precision drops the on-disk size by roughly 4x with a small accuracy hit (~0.005 Brier). TF.js dense models quantize to 8-bit with negligible accuracy loss. Both options are exposed in the brick publish settings on Workshop — try the quantized version first; it's almost always good enough.

Feature shape: where the choice really comes from

The honest decision tree:

  1. Are your inputs tabular with hand-engineered features (rolling averages, ratings, situational flags)? → XGBoost.
  2. Are your inputs sequences (last-N games, play-by-play windows, in-game time series)? → TF.js with a recurrent or convolutional front end.
  3. Are your inputs categorical with very high cardinality (player IDs, team-season IDs)? → TF.js with embedding layers.
  4. Do you have under 500 training samples? → TF.js with strong regularization, or fall back to a logistic regression baseline.
  5. Do you need to deploy to mobile? → TF.js.
  6. Do you need sub-20ms in-browser inference? → TF.js on WebGL.
  7. None of the above? → XGBoost, default to the tabular winner.

Worked example: NBA player props

For an NBA points prop model, the natural inputs are a player's last 10 games as a sequence (points, minutes, FGA, FG%, opponent eFG% allowed). XGBoost can use this if you pre-aggregate (mean, std, slope), but a small TF.js GRU + dense head can learn the temporal pattern directly:

  • XGBoost on aggregated features: Brier 0.2410, log loss 0.6839.
  • TF.js GRU(16) → Dense(8) → output: Brier 0.2371, log loss 0.6781.

The TF.js net wins by 1.5% log loss because the sequence captures pace-of-game effects that pre-aggregation washes out. Same dataset, opposite winner from the NFL spread example. Match the model to the feature shape, not the other way around.

Worked example: MLB run-line models

For MLB run lines, your features are mostly tabular: starter ERA, bullpen FIP, park factor, ump factor, lineup wOBA, lefty/righty splits. There are no sequences worth modeling at the game level. XGBoost dominates:

  • XGBoost (400 trees, depth 5, lr 0.05): Brier 0.2422, log loss 0.6852.
  • TF.js (32-16, dropout 0.2): Brier 0.2459, log loss 0.6895.

That's a 1.6% log-loss gap, which is gigantic in MLB run-line land where edges are razor-thin. If the brick is MLB-only, don't bother with TF.js unless you're explicitly building a sequence-aware version.

When to stop arguing and stack

The right answer to "which is better" for a serious bettor is often "both, stacked." A logistic-regression meta-learner trained on out-of-fold predictions from an XGBoost base and a TF.js base typically beats either base model by 5–10 basis points of Brier. The full recipe is in the dedicated stacking guide. The cost is one extra training pass; the payoff is a stack that's robust to whichever individual model rots first.

Practical workflow on Workshop

The standard build pattern on Workshop:

  1. Sketch a TF.js MVP — it trains in 30 seconds and gives you a baseline.
  2. If the baseline is in a reasonable range, train an XGBoost variant on the same features.
  3. Compare Brier and calibration. If XGBoost wins by > 0.005 Brier and you can pay the memory cost, ship XGBoost.
  4. If they're within 0.005, stack them and ship the stack.
  5. If TF.js wins, you probably have sequence features and should expand the architecture, not replace it.
  6. Push the chosen brick to Tinker for end-user testing on a small bankroll before scaling.

This workflow keeps you honest. Most bettors who build models default to whichever framework they're more comfortable with and then defend it after the fact. The matrix above gives you a defensible reason to pick one or the other, plus an exit ramp into stacking when the choice is genuinely close.

Where each one breaks

XGBoost in the browser breaks loudly: out-of-memory tab crashes, multi-second cold starts, and quantization that silently drops accuracy. TF.js breaks quietly: gradient instability, dead ReLU units, and training that "converges" to a slightly miscalibrated model that looks fine until you check the Brier score and the calibration plot. Both deserve a sanity check on a holdout fold and a calibration curve before you bet a dollar.

Architecture-level decisions inside TF.js

Choosing TF.js doesn't end the design decisions. Within TF.js, the architecture choice matters as much as the framework choice:

  • Dense feed-forward: default for tabular features. 1–3 hidden layers, ReLU activations, dropout 0.2–0.3. Fast to train, easy to debug.
  • GRU or small LSTM: for sequence inputs (last-N games). Use units=16–32; bigger units overfit quickly on betting data.
  • 1D convolution over time: for play-by-play windows. Convolutional kernels can capture temporal patterns more efficiently than recurrent units when sequences are long (50+ steps).
  • Embedding layers: for player/team IDs. Embedding dimension 4–8 is usually enough for sport-specific cardinality.
  • Attention layers: rarely worth it for betting. Adds parameters and inference cost without meaningful Brier improvement on most sport datasets.

Default to dense unless your input shape forces something else. Most "fancy architecture" in betting models is the modeler scratching an itch, not a Brier improvement.

XGBoost-specific tuning

If you've chosen XGBoost, the hyperparameters that actually matter for sports betting models:

  1. Number of trees: 300–800. More than 800 overfits without major regularization.
  2. Max depth: 4–7. Trees deeper than 7 memorize the training set.
  3. Learning rate: 0.02–0.08. Lower needs more trees but more stable.
  4. min_child_weight: 3–10. Prevents creating splits on tiny subsets.
  5. Subsample: 0.7–0.9 for column and row sampling each.
  6. L1 and L2 regularization: leave default, tune only if cross-validation Brier is unstable.

Grid search across these takes 5–10 minutes in the browser for a 5,000-game NFL spread dataset. Worth doing once per major retrain; not worth doing weekly.

The dataset-size cutoff

Below 500 games, neither model type is great. Above 5,000 games, both stabilize. Between those is the awkward middle zone where XGBoost can find more signal but also overfits faster. A useful heuristic: if you're in the awkward middle, train both, stack them with a high-regularization meta-learner, and the stack will hide the individual instability.

Edge cases worth knowing

  • Heavy class imbalance: UFC moneylines on big favorites are 75/25 splits. Stratified sampling helps both XGBoost and TF.js; using scale_pos_weight in XGBoost or class_weight in TF.js loss works similarly.
  • Highly correlated features: XGBoost handles them gracefully (trees pick one and ignore the redundant). Dense nets sometimes diverge on highly correlated inputs without normalization.
  • Missing data: XGBoost handles natively (splits on missingness). TF.js requires explicit imputation upstream — common error point.
  • Categorical features with high cardinality: one-hot encode for XGBoost (fine for under 100 categories); use embedding layers in TF.js for higher cardinality.
  • Live model retraining: TF.js can do incremental gradient updates from new data without a full retrain. XGBoost cannot — full retrain only.

Bottom line

XGBoost is the default for tabular features with engineered inputs and a desktop-first audience. TF.js is the default for sequence inputs, mobile-first delivery, and low-latency live use. The two stack well, and the stack is usually the right answer for a serious bettor. Start with the matrix in this post, build the brick on Workshop, and validate calibration on a holdout fold before betting a dollar.

Bet responsibly — set limits, never chase losses.

Named modeling examples

A model page is more useful when the feature examples are concrete. Josh Allen rushing attempts, Ja'Marr Chase target share, Nikola Jokic assist rate, Tarik Skubal strikeout projection, Igor Shesterkin starter confirmation, and Islam Makhachev control time are all different prediction problems. A single “player form” feature cannot explain them all, so the model needs sport-specific inputs and review notes.

  • NFL: separate route participation, pressure rate, and red-zone role from box-score volume.
  • NBA: separate usage, minute projection, pace, and back-to-back fatigue.
  • MLB: separate starter skill, handedness, park, weather, and lineup confirmation.
  • NHL and UFC: late confirmations and fight-week news can matter more than a season average.

Model inputs worth naming

Use names as evidence, not decoration. The useful SEO win is that Josh Allen, Ja'Marr Chase, Bijan Robinson and Puka Nacua and Chiefs, Bills, Eagles and Lions appear inside decisions, thresholds, and internal links instead of being dumped into a keyword list.

  • NFL model: route participation for Ja'Marr Chase, rushing attempts for Josh Allen, pressure rate allowed by the Bengals, and red-zone carry share for Jonathan Taylor should be separate features.
  • NBA model: usage, projected minutes, rest, and pace should move Nikola Jokic or Shai Gilgeous-Alexander props differently than a one-number power rating.
  • MLB model: Tarik Skubal strikeout projection, Coors Field park factor, lineup confirmation, and bullpen rest need their own columns.
  • Review loop: grade entry price, closing price, bet result, and model error separately so lucky results do not hide bad forecasts.

Build or audit the workflow in Tinker and review it with CLV.

Research note board

Use this model-audit board to keep features, validation, and bet sizing from collapsing into one confidence score.

Model layerWhat to inspectExample inputDowngrade when
FeatureWhether the variable maps to the sport and marketJosh Allen role data or hold price movementThe feature is a proxy for something you can measure directly
ValidationOut-of-sample error, CLV, calibration, missing dataChiefs market movement after injury newsWins come without beating the close or improving calibration
SizingBankroll, confidence interval, correlation, market limitmoneyline exposure compared with related ticketsMultiple bets repeat the same thesis at full stake

Model calibration: predicted vs observed

Predicted win probability bucket vs the empirical win rate inside that bucket on the test set. Points on the y=x reference line are perfectly calibrated; points below mean the model is overconfident in that bucket.

EV per $100 across win rate × odds grid

Expected value of a $100 stake at each combination of true win rate and market odds. Anywhere the cell is positive you have a long-run profitable bet; the magnitude shows how aggressive Kelly will size it.

Frequently asked questions

Which model type is more accurate for sports betting in general?
On tabular sports data with well-engineered features (rolling averages, opponent-adjusted ratings, market features), gradient-boosted trees like XGBoost are typically 1–3% better in log loss than a similarly-sized feedforward neural network. On raw or near-raw inputs (sequence of last-N games, play-by-play summaries), a TF.js net with the right architecture can match or beat XGBoost because it learns the engineering implicitly. The honest answer is: for the kind of dataset most retail bettors assemble (a few hundred games, 15–30 hand-crafted features), XGBoost wins on accuracy. For richer datasets or sequence inputs, the gap closes.
How big are these models when loaded into the browser?
A 500-tree XGBoost model with depth 6 ports to roughly 70–120MB of JSON for browser inference, and spikes to about 1.5x that during prediction batches. A typical TF.js feedforward net with 64-32-16 hidden units stays under 5MB on disk and under 15MB in memory during inference. For mobile-first deployments, a quantized XGBoost (3-bit splits) can drop to 20–30MB but with measurable accuracy loss; an unquantized TF.js net of the same accuracy budget is usually friendlier.
How long does training each take on a normal laptop CPU?
A 500-tree XGBoost model on 2,000 NFL games with 20 features trains in 2–5 seconds on a modern laptop CPU using the WebAssembly port. A TF.js dense net with similar capacity trains in 20–40 seconds for 200 epochs with batch size 32 on the same hardware. Both are fast enough for client-side use; XGBoost is the faster trainer, TF.js is the faster inference engine.
Can I export an XGBoost model trained in Python and run it in TF.js?
Not directly. XGBoost has its own JSON serialization format. You can port the trained trees into a JS runtime (xgboost-wasm, tract-xgb), but you cannot drop them into TensorFlow.js — different graph representations. If you need cross-runtime portability, train in Python with XGBoost, export via ONNX, and run via ONNX-web in the browser. We do this regularly when a model is too heavy to train client-side but the inference path stays in the browser.

Build a free model in 60 seconds →

Go →
10m read time
28 players/teams
8 key angles
Angles in this read 6 angles

Editorial example layer

Concrete examples make the page useful: tie player and team names to role, price, matchup, and timing so the content reads like analysis instead of glossary filler.
Patrick MahomesJosh AllenLamar JacksonJoe BurrowJalen HurtsJustin HerbertC.J. StroudTua TagovailoaChiefsBillsRavensEaglesLionsBengalsclosing line valuetarget shareair yardsred-zone roleroute participation
XGBoost vs TF.js Bricks: Which to Use When data infographic
Chart view of the article's core numbers. Source: inline-lib-modelVsMarketCalibration-xgboost-vs-tfjs-bricks.

Get picks in your inbox

One email, every slate — ranked edges, no touts. Unsubscribe any time.

Start free — pick a sport

Go →

We use cookies for essential site functionality. With your consent, we also use cookies for analytics and performance monitoring. See our Privacy Policy.